Skip to content
bigrobot.
Performance

Core Web Vitals in practice: what actually moves LCP, INP and CLS on a Next.js site

A store scoring 96 in Lighthouse on a developer laptop was reporting an INP of 412 ms in Search Console, rated poor across 68 per cent of URLs. The cause was not in the application code at all: three tags fired through Tag Manager and a chat widget. This is the order in which we check and fix things.

Marta Zawadzka9 min read

Field data first, lab data second

Lighthouse simulates one device, one connection and one page load with no extensions installed. CrUX reports what actually happened to Chrome users over the previous 28 days, at the 75th percentile. Those two numbers can differ by a factor of two and both are correct; they answer different questions. Make decisions on CrUX and treat Lighthouse as a way of diagnosing a single change.

For ongoing visibility we poll the PageSpeed Insights API daily for the twenty most important page templates and write the results to a sheet. For deeper diagnosis we ship the web-vitals library in its attribution build, which returns not only the metric value but the element responsible for LCP and the script that blocked the main thread. Without attribution, any conversation about INP is guesswork.

In Next.js, useReportWebVitals sends this to your own endpoint. A small receiver writing into an analytics table, split by page template and device type, is enough. After two weeks you have a picture no external audit can give you, because you are looking at a distribution rather than an average that describes nobody.

LCP is nearly always an image or your server

LCP decomposes into four parts: time to first byte, resource load delay, resource load duration and element render delay. Splitting them tells you immediately where to look. If TTFB is 900 ms, optimising the image is pointless until the server-side caching layer is sorted. In practice the winning combination is usually incremental regeneration with a short revalidation window plus s-maxage with stale-while-revalidate at the CDN.

The second common failure is a hero image that only starts loading after hydration, because it sits in a client component or a carousel. next/image with the priority attribute sets fetchpriority to high and adds a preload, but only when the element is present in the initial HTML. A client-rendered carousel pushes the load start out by 400 to 800 ms, and no amount of format tuning recovers that.

Web fonts add 200 to 300 ms to text render when they come from a third-party host. The next/font module fetches them at build time, serves them from your own origin and generates matching fallback metrics, which removes a class of layout shift as a side effect. If you must stay with an external provider, add preconnect and set display to swap, but accept the worse result.

  • Break LCP into its four sub-parts before changing anything
  • The LCP image must be in the initial HTML, with priority and correct sizes
  • Set s-maxage and stale-while-revalidate at the CDN for category pages
  • Use next/font rather than a third-party font host
  • Check that the CDN is not serving an uncompressed variant to some clients

INP is hydration cost plus other people's scripts

The good threshold for INP is 200 ms at the 75th percentile, and most shops miss it for one reason: the main thread is busy exactly when the first interactions arrive. The classic case is a user tapping a category filter two seconds after load, while a list of sixty products is still hydrating. Server components in the App Router remove a large share of this, simply because they ship no hydration code.

Third-party scripts are usually a bigger offender than your own code. A Tag Manager container with eight tags, a heatmap tool, a chat widget and a reviews script can occupy the main thread for 1.2 seconds in blocks of 150 to 300 ms. Moving chat and reviews to next/script with lazyOnload, and shifting non-critical tags onto a scroll-based trigger, typically buys 80 to 150 ms of field INP. Partytown looks attractive but breaks on scripts that use document.write or synchronous cookie access, so we treat it as an option rather than a default.

Inside the application itself, the biggest win comes from breaking up long tasks. If a filter click runs 40 ms of computation and then re-renders the entire list, separate the immediate visual response from the heavy work by yielding with scheduler.yield or startTransition. The user sees the filter tick within 30 ms and the list arrives 200 ms later, which reads as fast.

A performance budget in CI, or it all comes back

Optimisation without an enforcement mechanism survives about three months. Then someone adds a 90 kB charting library, someone else drops in another marketing script, and you are back where you started. So every project ends with thresholds in the pipeline: Lighthouse CI with assertions on LCP and CLS, plus a hard limit on first-load JavaScript.

Our default numbers: first-load JavaScript under 170 kB compressed on listing pages, a 120 kB AVIF budget for the hero image, and no task longer than 200 ms in the mobile simulation. Thresholds must fail the pull request rather than print a warning, because warnings get ignored. Once a quarter we review them and tighten where there is headroom.

How much of this shows up in rankings and revenue

Core Web Vitals are part of the page experience signals, and the ranking effect is real but weak and never sufficient on its own. A page that does not answer the query will not reach the top three because its LCP improved by 400 ms. Treating these metrics as a ranking lever ends in disappointment and in budget spent where there is nothing to recover.

The conversion argument is far easier to defend. On two projects last year, moving INP from around 380 ms to under 200 ms coincided with a 6 to 9 per cent drop in abandonment at the delivery step, measured with a split test at the deployment level. Those numbers do not transfer directly to another store, but the direction repeats, and the reason is simple: an interface that does not respond to a tap reads as broken.

Key takeaways

  • Decide on CrUX data; Lighthouse is for diagnosing a single change.
  • Break LCP into four sub-parts before you start optimising images.
  • INP on retail sites is mostly third-party scripts and hydration of large lists.
  • An overlay consent banner and correctly sized skeletons remove most CLS.
  • Without CI thresholds, a performance fix lasts roughly one quarter.

Common questions

Yes, but as one of the weaker page experience signals. Between two pages that answer the query equally well the faster one can win, yet improving the metrics will not move a page that does not match intent. The stronger case for Core Web Vitals is conversion rate and the cost of paid traffic.

Under 200 ms at the 75th percentile of field data is rated good, 200 to 500 ms needs improvement and anything above 500 ms is poor. Look at mobile separately, because the threshold is often breached there even when the site-wide figure looks acceptable.

No. It gives you good tools: the image component, build-time font loading, server components and code splitting, but each of them has to be used deliberately. A Next.js project with a heavy client component above the fold and four marketing scripts will score worse than a carefully built site on any other stack.

It starts with an audit

Tell us in two sentences what you want to achieve. We reply within one business day.