100/100 Core Web Vitals on WordPress

100/100 Core Web Vitals on WordPress

Last verified: September 22, 2026
13 min read
Guide
Core Web Vitals

The first thing we ran on this store was not a plugin. It was a single Lighthouse pass on a mid-tier Android profile, and then, ten minutes later, the same URL in the CrUX report. The two numbers did not agree, and the gap between them is the entire reason this project took four weeks instead of an afternoon.

Lighthouse said 42 on mobile. It also said LCP 4.8s, INP 450ms, CLS 0.25. That is a lab run: one synthetic device, one throttled connection, one cold cache, no logged-in session, no cart, no consent banner that has already been dismissed. The field data said something different and less flattering. Real visitors were spread across a long tail of devices and networks, and the 75th percentile, which is the number Google actually uses, sat worse than the lab run on INP and better on LCP. That inversion is normal and it is diagnostic: a lab LCP worse than field LCP usually means the synthetic throttle is harsher than your real audience; a field INP worse than lab INP almost always means the lab never clicked the thing that hurts.

So the working rule for this rebuild, and for every one since: the lab tells you what to fix, the field tells you whether you fixed it. Lighthouse is a debugger, not a scoreboard. It gives you a trace, a filmstrip and an attribution chain. CrUX gives you a verdict, twenty-eight days late, on real hardware. Anyone optimizing against the score alone ends up tuning the audit rather than the site.

The store itself was in the home decor category, a WooCommerce build with the usual archaeology: a page builder, a slider plugin, four analytics tags, a chat widget and an object cache that had been switched off during a migration and never switched back on.

#Where the first report actually pointed

The Lighthouse score is a weighted average, and the weighting matters more than people expect. Total Blocking Time carries the largest single share of the performance score, LCP is next, CLS is comparatively cheap. A site can sit at 42 and have a perfectly fine LCP, purely because the main thread is jammed. Chasing the score without opening the trace means you may spend a week on images and move nothing.

The three things we read before touching any code:

  1. The LCP element and its attribution. Chrome tells you which node it picked and splits the time into time to first byte, resource load delay, resource load duration and render delay. Those four buckets have four different fixes, and three of them have nothing to do with image size.
  2. The long tasks list in the performance panel. Anything over 50ms is a task that can block an interaction. We had eleven of them above 200ms during page load.
  3. The layout shift cluster timeline. CLS is a session window score, not a single event, so it matters whether your 0.25 is one big shift or fifteen small ones. Ours was two clusters: one at font swap, one when the lazy-loaded product grid resolved.

#LCP is four numbers pretending to be one

The obvious villain was a Revolution Slider hero pulling roughly 4MB of JavaScript before the first image painted. Obvious villains are satisfying and usually incomplete.

Breaking the 4.8s down, the image byte size was not even the largest contributor. Time to first byte was about 600ms on an uncached origin. Resource load delay, the gap between navigation start and the browser even discovering the hero image, was over a second, because the image was not in the HTML at all: the slider injected it from JavaScript after its own bundle parsed. You cannot preload what you cannot see, and the preload scanner, the thing that makes browsers feel fast, was blind to it.

What we did, in the order that mattered:

  • Removed the slider entirely and replaced it with a static CSS grid hero. One image, one heading, one button, in the HTML, discoverable by the preload scanner on the first byte of the response.
  • Marked the hero fetchpriority="high" and removed loading="lazy" from it. Lazy loading an LCP image is one of the most common own goals in WordPress, because a well-meaning plugin applies it site-wide including above the fold.
  • Converted source images to AVIF with a WebP fallback through <picture>. The header image went from roughly 800KB as PNG to about 45KB. AVIF encoding is slow, so this runs as a build step or a queued job, never on upload in a request cycle.
  • Cut the render-blocking CSS path. The theme shipped one stylesheet of several hundred kilobytes. We inlined the critical rules for the hero and deferred the rest with media="print" onload="this.media='all'". For dynamic product templates the critical set differs per template, so the generator runs per template type, not per URL, which keeps the cache key count sane.

The trade-off worth naming: font-display: optional and aggressive critical CSS both make the first paint faster and the first paint uglier. If your brand team cares about the exact typeface being visible in the first 100ms, you are buying a layout shift, and you should decide that deliberately rather than discover it in a CrUX report.

After this pass the lab LCP landed near 1.9s, still short of where it ended, because the remaining time was server side and we had not touched the server yet.

#The shifts came from type, not from images

Missing width and height on images is the textbook CLS cause, and it was present here, but it was the smaller of the two clusters. The larger one was the font swap. The body text rendered in the fallback, then the custom font arrived and reflowed the paragraph by roughly ten pixels, which on a product listing cascades down the whole column.

Two fixes, both boring:

  • <link rel="preload" as="font" crossorigin> for the one weight used above the fold, and font-display: optional on that face. Optional, not swap. With optional the browser gives the font a 100ms window and, if it misses, keeps the fallback for the rest of that navigation. No shift, at the cost of some first-visit visitors seeing the fallback.
  • Fallback metric overrides (size-adjust, ascent-override) so the fallback occupies nearly the same box as the real face. This is the step most teams skip, and it is what turns a visible reflow into an invisible one.

For images, aspect-ratio on the container plus intrinsic width/height attributes. WordPress emits those attributes by default; page builders frequently strip them, which is worth checking rather than assuming.

CLS went to 0.00 in the lab and stayed effectively flat in the field, which is the harder half: field CLS includes cookie banners, A/B test flicker and ad slots that a lab run with a clean profile never sees.

#INP: the main thread was busy tracking the visitor

INP replaced FID in March 2024, and the replacement was not cosmetic. FID measured only the delay before the first input handler started. INP measures the full path from input to the next paint, across the whole session, and reports near the worst interaction. A site could score a green FID and still feel broken. Most WordPress sites that “suddenly failed Core Web Vitals in 2024” did not get slower; they started being measured honestly.

The bottleneck here was a familiar stack: Google Tag Manager loading three further tags, a chat widget, a heatmap recorder, and WooCommerce’s own cart fragments polling. Tapping the menu on a mid-range phone queued behind whatever the tag manager was doing.

Partytown moved the third-party tags into a Web Worker. The mechanism is worth understanding before you adopt it: the worker cannot touch the DOM, so Partytown proxies DOM access back to the main thread through synchronous XHR against a service worker. It works, and it is genuinely effective at clearing main-thread time, but it is not free.

What it costs you, plainly:

  • Scripts that expect synchronous DOM reads can break in ways that only show up in production, because the proxy is not a perfect emulation.
  • You need the service worker and the ~partytown assets served from your own origin, with correct headers. Get the headers wrong and it silently falls back to the main thread, which looks like it works and does nothing.
  • Debugging moves into a worker context, so your existing error reporting may stop seeing those exceptions.

We also did the unglamorous half: deferred the chat widget until first interaction or a scroll threshold, removed the heatmap recorder from product pages entirely, and yielded long tasks with scheduler.yield() where supported so the browser can service an input mid-task instead of after it.

Lab INP came down to roughly 48ms. Field INP took longer to move, because the field number is a 28-day trailing percentile and it only reflects visitors who actually interacted.

#Cart fragments, the WooCommerce specific tax

Every WooCommerce theme that shows a live cart count calls wc-ajax=get_refreshed_fragments on each page load. It is uncached by definition, it hits PHP and the database, and on a store with a heavy session table it can take longer than the rest of the page. It also runs on pages where nobody is looking at the cart, such as blog posts and the contact page.

The mechanism, not a percentage: we scoped the fragments request out of pages with no cart UI, cached the fragment response in sessionStorage for the duration of a visit, and let it revalidate only after a cart-changing action. The trade-off is a cart badge that can be stale for one navigation if the customer has two tabs open, which for this store was an acceptable exchange and for a store with real-time stock messaging would not be.

#Server side sets the floor for everything above it

You cannot reach the top band of these metrics on the cheapest shared plan, and the reason is not marketing. TTFB is the first term in the LCP sum. If the origin needs 600ms to produce HTML, the fastest possible LCP is 600ms plus network plus decode plus paint, no matter how small your image is.

Three changes, in ascending order of effect:

  1. Redis object cache, restored. The menu tree, the options table reads and the term queries stop being database round trips. On WooCommerce this matters most on category pages, where the term and meta queries multiply.
  2. Full-page edge caching with a cookie-aware bypass, so anonymous visitors get HTML from a nearby edge node and logged-in or cart-carrying sessions go to the origin. The hard part is not the caching, it is the bypass rules: get them wrong and you serve one customer’s cart to another, which is a far worse outcome than a slow page.
  3. Serving the HTML from a location near the audience rather than from a datacentre chosen years earlier for unrelated reasons. TTFB went from roughly 600ms to roughly 40ms for cached anonymous responses.

Worth stating plainly: edge caching a WooCommerce store is a correctness problem before it is a performance problem. Budget the testing, including a logged-in session, a partially filled cart, a coupon, and a checkout under a second browser profile.

#Speculation rules, and how not to prerender your own checkout

The Speculation Rules API is the piece that changes how the site feels rather than how it scores. A JSON block in the document tells Chrome which same-origin URLs to prefetch or prerender, and on what eagerness. Prerendering a product page while the visitor hovers its card means the navigation is already complete when they click.

{
  "prerender": [{
    "where": { "href_matches": "/product/*" },
    "eagerness": "moderate"
  }],
  "prefetch": [{
    "where": { "href_matches": "/*" },
    "eagerness": "conservative"
  }]
}

Three practitioner notes that cost us time:

  • Exclude cart, checkout, account and logout. A prerender executes the page, including its analytics and any side effects. Prerendering a logout link logs the visitor out. Use not: { href_matches: [...] } and test it.
  • Prerendering costs the visitor bandwidth and the origin requests. eagerness: "moderate" (hover) is a reasonable default; "eager" on a large catalogue will multiply your origin traffic for pages nobody opens.
  • Analytics needs the Page Visibility API. A prerendered page that fires a pageview on load inflates your numbers with pages that were never shown. Defer the beacon until prerenderingchange or until the document becomes visible.

Support is Chromium-only at the time of writing, so treat it as a progressive enhancement for the majority of a typical e-commerce audience, not as a substitute for the work above it.

#What we would do differently next time

Two things. First, we removed the slider before measuring what a properly configured static hero would have scored, so we never learned how much of the 4.8s was the slider plugin itself versus the pattern of injecting the hero from JavaScript. Second, we ran the Partytown migration and the third-party cleanup in the same week, which means we cannot attribute the INP improvement between them. Ship one lever per measurement window when you can; the temptation to batch is strongest exactly when the deadline makes attribution most valuable later.

#Why this is worth doing at all

Not for a green badge in a slide deck. The mechanism by which speed pays back is narrow and specific, and it is worth stating without inventing numbers for it.

A faster LCP means fewer visitors abandon before the page is useful, because abandonment rises with waiting time in a way that is well documented across the industry. A better INP means fewer repeated taps on a menu or an add-to-cart button, and fewer support messages about a button “not working” when in fact it worked three times. A stable CLS means fewer mis-taps, which on a checkout form is the difference between a completed order and a bounced one. On the paid side, landing page experience feeds Google Ads Quality Score, so a faster landing page can lower what you pay for the same click. And Core Web Vitals is a ranking signal in the page experience group: it is a tiebreaker between comparable results, not a substitute for relevance.

None of those are promises of a specific percentage, and anyone offering you one without your own before-and-after measurement is selling something. The honest version is that you can measure your own baseline in an afternoon, in the lab and in the field, and then attribute each change to a number you took yourself.

Is your WooCommerce site losing orders to slowness? WPPoland works from the trace, not from the score. After lab scores go green, cart fragments and checkout weight still need the layered playbook in WooCommerce performance optimization 2026.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Related cluster

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

Article FAQ

Frequently asked questions

Practical answers to apply the topic in real execution.

SEO-readyGEO-readyAEO-ready3 Q&A
Did you use WP Rocket?#
Yes, but as a baseline. The 100 score required custom code for 'Critical CSS' generation on dynamic product pages and Speculation Rules for prefetching.
How did you fix INP on WooCommerce?#
The 'Add to Cart' AJAX button was the bottleneck. We refactored it to use a Web Worker (Partytown) to keep the main thread idle.
Is this possible on Shared Hosting?#
Extremely difficult. This result relied on Redis for Object Caching and a CDN for Edge Caching. Shared hosting TTFB (Time to First Byte) is usually too slow for a 100 score.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles

Too many WordPress plugins

An insurance comparison site arrived with 30+ plugins, a 705 MB database, and a 7.7s LCP. The worst offender was a view counter writing to wp_postmeta on every load. A real teardown of the plugin-sprawl pattern that fast and AI-assisted builds keep producing.