Core Web Vitals for React and Next.js Developers: LCP, INP and CLS Explained with Fixes

Core Web Vitals for React and Next.js developers - CodingHumans

Core Web Vitals are three metrics Google uses to measure how a page feels to real users. How fast does the main content load? How quickly does the page respond when you tap something? Does the layout stay still? They affect search ranking, but more importantly they match what users actually feel. They also come up often in frontend interviews.

This post covers each metric, its thresholds, how to measure it properly, and the fixes that help most in React and Next.js apps.

1. The three metrics and their thresholds

Core Web Vitals thresholds for LCP, INP and CLS
A page passes when the 75th percentile of real visits is "good" for all three.
MetricMeasuresGoodPoor
LCP (Largest Contentful Paint)Time until the largest image or text block in the viewport is rendered≤ 2.5 s> 4 s
INP (Interaction to Next Paint)Delay between a user interaction (click, tap, key press) and the next frame being painted, typically reported as the worst interaction of the visit≤ 200 ms> 500 ms
CLS (Cumulative Layout Shift)How much visible content moves unexpectedly, scored from the largest burst of layout shifts≤ 0.1> 0.25

INP replaced First Input Delay (FID) as a Core Web Vital in March 2024. FID only measured the wait before the first interaction's handler started. INP looks at every interaction and also counts processing and rendering time. That makes it much harder to pass if your app runs a lot of JavaScript.

2. Field data vs lab data

  • Field data (real user monitoring) comes from real visitors on real devices and networks. Google's Chrome User Experience Report (CrUX), which you see in PageSpeed Insights and Search Console, is field data. This is what counts for ranking.
  • Lab data comes from a simulated run, like Lighthouse in Chrome DevTools. It is great for debugging and for catching regressions, but it is not what your real users see.

Lighthouse cannot measure INP, because nobody clicks anything during the test. Instead it reports Total Blocking Time (TBT) as a stand-in. To debug INP, open the Performance panel in DevTools and use the page yourself. Turn on CPU throttling so it behaves more like a mid-range phone.

3. Improving LCP

LCP time has four parts: server response time (TTFB), the wait before the LCP resource starts loading, the time to download it, and the time to render it. Fix whichever part is biggest first.

  • Find the LCP element. The DevTools Performance panel and PageSpeed Insights both tell you which element it is. On most pages it is a hero image or a heading.
  • Let the browser find the LCP image early. It should be a normal <img> in the server HTML, not a CSS background or something added later by client JavaScript. Give it fetchpriority="high" and never lazy-load it. With next/image, mark the hero image as a priority image so Next.js preloads it.
  • Serve the right size and format. Use responsive srcset/sizes and modern formats like WebP or AVIF. next/image does this for you.
  • Render on the server. A client-only React app shows nothing useful until its JavaScript downloads, parses and runs. Server rendering, static generation or Server Components put real content in the very first HTML response.
  • Reduce TTFB. Cache pages at the edge or on a CDN, use static generation where the data allows it, and avoid slow one-after-another data fetching before the first byte. Streaming with Suspense lets the page shell, including the LCP element, arrive before the slower sections.
  • Fonts. If the LCP element is text, make sure web fonts don't hold it back. next/font hosts fonts on your own domain and saves extra network round trips.

4. Improving INP

INP is all about the main thread. If a user clicks while a long JavaScript task is running, the browser can't respond until that task ends. Then your event handler runs, then React re-renders, then the browser paints. All of these add up.

  • Ship less JavaScript. Every kilobyte has to be parsed and run. Hydrating a large tree blocks the main thread right when users start clicking. Use Server Components for non-interactive UI, code-split heavy widgets with next/dynamic or React.lazy, and check your large dependencies.
  • Keep event handlers light. Update the UI first and do the expensive work after that. Push analytics and other non-urgent work to later.
  • Mark non-urgent updates as transitions. With useTransition and useDeferredValue, React keeps the input responsive while a large list re-renders in the background.
  • Make re-renders cheaper. Keep state close to where it is used, don't put fast-changing values in a top-level context, memoise expensive children, and virtualise long lists.
  • Break up long tasks. Split the work into chunks and give the main thread a break between them (for example with scheduler.yield() where supported, or setTimeout). Or move heavy computation to a Web Worker.
  • Watch third-party scripts. Chat widgets, tag managers and ads are often behind the longest tasks. Load them after the page becomes interactive (next/script with lazyOnload or afterInteractive).

5. Improving CLS

  • Always reserve space for images and video. Set width and height attributes (or a CSS aspect-ratio) so the browser knows the size before the file arrives. That is why next/image asks for dimensions.
  • Reserve space for ads, embeds and banners. Give their containers a minimum height. Never push new content in above existing content, unless the user asked for it with an action.
  • Load fonts without big reflows. When a font swaps in, text can change size. next/font uses a size-adjusted fallback font to keep that shift small.
  • Animate with transforms. Animate transform and opacity, not properties like top, height or margin, which cause layout work.
  • Avoid swapping content on the client right after load. If the server renders a placeholder and the client shows something else after hydration (say, based on localStorage), the layout shifts. Use skeletons with the same size as the real content.

6. Monitoring in a Next.js app

Lab tests alone won't tell you what real users go through. Collect field data with the web-vitals library, or use the hook Next.js gives you in a small client component:

'use client';
import { useReportWebVitals } from 'next/web-vitals';

export function WebVitals() {
  useReportWebVitals((metric) => {
    // metric.name: 'LCP' | 'INP' | 'CLS' | 'FCP' | 'TTFB'
    navigator.sendBeacon('/api/vitals', JSON.stringify(metric));
  });
  return null;
}

Send the values to your analytics. Then track the 75th percentile for each page type and device class. Check slow pages on mobile first, because that is where most failures hide.

Key takeaways
  • LCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.1, measured at the 75th percentile of real visits.
  • Field data decides ranking. Lab tools are for debugging, and they cannot measure INP directly.
  • LCP: server-render the content, prioritise the hero image, cut TTFB.
  • INP: ship less JavaScript, keep handlers light, use transitions, break long tasks.
  • CLS: reserve space for everything that loads late.

Interview questions and FAQ

Why did INP replace FID?

FID only measured the input delay of the first interaction, so most sites passed it easily. INP measures the full delay of interactions across the whole visit. That gives a much truer picture of how responsive the page is.

Why does my Lighthouse score look good but Search Console shows poor Core Web Vitals?

Lighthouse is one simulated run on one device. Search Console uses data from real users. They may be on slower phones and networks, and they actually use the page, which affects INP and CLS after load.

Does lazy-loading images always help?

Only for images below the fold. Lazy-loading the LCP image delays it and makes LCP worse.

How does server rendering affect INP?

Server rendering shows content earlier. But if the page then hydrates a large JavaScript bundle, the main thread is busy exactly when users start clicking. Cutting client JavaScript, for example with Server Components, helps both LCP and INP.

Comments

Answered 0/0Correct 0