logo

Imad Attif

Core Web Vitals Explained: What Each Metric Actually Measures

Imad Attif, Sr. Frontend Engineer

17 min read

Aug 4, 2025

Core Web Vitals are Google's three metrics for how a page actually feels to a real user: how fast the main content shows up, how quickly the page reacts when you interact with it, and how much things jump around while it loads. They're also a search ranking signal, which is why most teams first meet them in an SEO report.

The problem is that most explanations stop at the acronyms. Knowing that "LCP should be under 2.5 seconds" doesn't tell you what LCP is actually timing, why yours is 4 seconds, or which fix will move the number.

This post explains what each metric really measures, what makes each one go bad, and the highest-impact fixes, with code. The short version:

  • LCP (Largest Contentful Paint) measures loading: how long until the biggest piece of content is visible. Good is under 2.5 seconds.
  • INP (Interaction to Next Paint) measures responsiveness: how long the page takes to visibly react when you click, tap, or type. Good is under 200 milliseconds.
  • CLS (Cumulative Layout Shift) measures visual stability: how much the layout jumps around unexpectedly. Good is under 0.1 (it's a score, not a time).

One important detail before we go metric by metric: these numbers come from real users. Google collects them from actual Chrome sessions (the Chrome User Experience Report, or CrUX), and your score is the 75th percentile. That means at least three quarters of your visitors must have a good experience for the metric to count as good. Your fast laptop on office wifi is not the measurement; someone's mid-range Android on hotel wifi is.

What are Core Web Vitals?

Core Web Vitals are the three-metric subset of Google's broader Web Vitals initiative, chosen to cover the three things users feel most: loading, interactivity, and stability. Each metric has three zones:

  • LCP: good under 2.5s, needs improvement up to 4s, poor above 4s.
  • INP: good under 200ms, needs improvement up to 500ms, poor above 500ms.
  • CLS: good under 0.1, needs improvement up to 0.25, poor above 0.25.

The set evolves. In March 2024, INP replaced the older FID (First Input Delay). If your dashboards or your knowledge still reference FID, they're out of date; INP is stricter and much harder to game, because it looks at all interactions rather than only the first one. That strictness shows in the data: INP is now the vital that sites most commonly fail.

A practical rule from the trenches: optimize when a metric is in the yellow or red zone, and stop when it's green. Performance work has diminishing returns, and a page with a 1.9s LCP has better uses for engineering time than chasing 1.6s.

Now the metrics, one at a time.

LCP: Largest Contentful Paint

What it measures

LCP is the time from when the user starts navigating to your page until the largest content element in the viewport finishes rendering. The browser tracks candidates as the page loads (an image, a video poster frame, a block of text) and reports the render time of the biggest one.

In practice, your LCP element is almost always the hero image or the main headline. You can see exactly which element it is: run Lighthouse or open the Performance panel in Chrome DevTools, and the LCP entry names the element.

LCP matters most of the three because it's the metric closest to business outcomes. It answers the user's first question, "is this page working?", and it's where slow sites lose people before anything else happens.

What makes LCP slow

LCP is the sum of everything that happens before the main content renders, so any of these can be the bottleneck:

  • Slow server response. The browser can't render what it hasn't received. A slow backend or no CDN delays everything downstream.
  • Render-blocking resources. The browser fetches your HTML, then discovers it needs a stylesheet, and makes a second round-trip before it can paint anything.
  • A slow-loading hero image. A 2 MB PNG discovered late in the parse.
  • Client-side rendering. If the page is an empty shell until a JavaScript bundle downloads, parses, and fetches data, LCP waits for that entire chain.

How to fix it

Attack the chain in order. First, serve HTML and assets from a CDN so the initial response is fast everywhere, not just near your server.

Second, make the hero image cheap and early. Use a modern format (WebP is a safe default with about 97% support; AVIF compresses photos even better at about 93%), size it for the viewport instead of shipping the original, and tell the browser it's the priority:

Third, unblock first paint. Inline the critical CSS (the styles the first screen needs) directly in a <style> tag in the HTML, and load the rest at low priority. Defer scripts that don't affect the first screen:

Fourth, don't let fonts block text. By default, a browser can hold text invisible for up to about 3 seconds waiting for a custom font. font-display: fallback (or swap) renders text immediately with a system font and upgrades when the custom font arrives.

If your LCP element is text rendered by JavaScript, the real fix is usually architectural: server-side rendering or static generation, so the HTML arrives with the content already in it.

In Next.js

Next.js covers most of this chain if you use its primitives instead of the raw HTML elements:

The priority prop does the preload and fetchpriority work from the snippet above, and next/image also handles responsive sizing and serves WebP/AVIF automatically. The rule: every LCP image should have priority; nothing else should.

Server-side rendering and static generation are the default in the App Router, so your LCP element usually arrives in the initial HTML. Fetch the data your hero needs in a Server Component rather than in a client-side useEffect, which would put a network round-trip between first paint and your LCP.

INP: Interaction to Next Paint

What it measures

INP measures how long the page takes to visibly respond to user interactions: clicks, taps, and key presses. For every interaction during the visit, the browser measures the time from input to the next frame the user sees. Your INP is roughly the worst of these (technically, a high percentile across all of them).

That "next paint" definition is what makes INP honest. It doesn't just measure when your event handler ran; it measures when the user saw something change. Three phases add up:

  1. Input delay. The main thread was busy, so the handler couldn't even start.
  2. Processing time. Your handler's actual work.
  3. Presentation delay. The browser re-rendering: style, layout, paint.

Notice that all three happen on or around the main thread. INP is fundamentally a "is your main thread free?" metric.

What makes INP bad

  • Long tasks. A single JavaScript task that runs for 400ms means every click during those 400ms waits.
  • Heavy handlers. Filtering a 50,000-item array, or triggering a re-render of an enormous component tree, synchronously inside a click handler.
  • Expensive rendering after the handler. Updating data that reflows a huge DOM. A click that inserts 2,000 list items pays for 2,000 elements of layout before the next paint.
  • Layout thrashing. Interleaved DOM reads and writes forcing repeated synchronous layout. (This is pipeline territory; see our post on the browser rendering pipeline for the full mechanics.)

How to fix it

The theme is: get off the main thread, or at least give it breathing room.

Break long tasks into chunks so the browser can paint and handle input between them:

Give instant feedback, then do the work. The user needs to see something within 200ms, not the finished result. Set the loading state, paint, then compute:

Beyond that: move genuinely heavy computation to a Web Worker so it can't block input at all. Keep the DOM small, and virtualize long lists so an interaction never triggers layout for thousands of nodes. Debounce input handlers that fire on every keystroke. And batch DOM reads and writes to avoid layout thrashing.

In Next.js

The biggest INP lever in Next.js is shipping less JavaScript to the main thread in the first place:

  • Keep components on the server. Server Components (the App Router default) send HTML, not JavaScript. Only components marked 'use client' add to the bundle that competes with your users' clicks. Resist the reflexive 'use client' at the top of every file.
  • Load heavy client components lazily with next/dynamic, so a chart library or rich-text editor doesn't block interactions on pages where it isn't used yet:
  • Mark expensive state updates as non-urgent with React's useTransition, so typing and clicking stay responsive while a large re-render happens in the background:

CLS: Cumulative Layout Shift

What it measures

CLS measures how much visible content moves around unexpectedly. Every time an element shifts position between frames without the user having interacted, the browser scores the shift: how much of the viewport was affected times how far things moved. CLS is the sum of these scores over the worst burst of shifting during the visit.

Unlike the other two vitals, CLS is not a time. A score of 0.1 roughly means a shift affecting 10% of the screen. And shifts within 500ms of a user interaction don't count, so an accordion expanding on click is fine.

Everyone knows this metric as a feeling: you go to tap a link, an ad loads above it, and you tap the ad instead.

What causes layout shifts

  • Images without dimensions. The browser renders the page with the image at zero height, the image arrives, and everything below it jumps down.
  • Ads, embeds, and iframes that inject themselves and push content aside.
  • Dynamically inserted banners (cookie notices, promo bars) at the top of the page.
  • Web fonts swapping in with different metrics than the fallback font, causing text to reflow.
  • Animating layout properties like height or top, which shoves neighbors around as the element grows or moves.

How to fix it

The single rule behind every CLS fix: reserve the space before the content arrives.

Always give images and videos their dimensions. Modern browsers compute the aspect ratio from the width and height attributes and reserve the right amount of space even in responsive layouts:

For dynamic content, reserve the slot explicitly:

For fonts, font-display: fallback plus tuning the fallback font's metrics (the size-adjust and ascent-override descriptors in @font-face) makes the swap nearly shift-free, because the fallback occupies the same space as the real font.

And for anything that moves or grows, animate transform instead of layout properties. A transform doesn't move neighbors, so it can't shift layout (and it's cheaper for the rendering pipeline too).

In Next.js

Two Next.js primitives eliminate the most common CLS sources by construction:

  • next/image requires dimensions (width/height or fill), so the zero-height-then-jump problem can't happen. The framework refuses to render an unsized image.
  • next/font fixes font-swap shifts automatically. It self-hosts the font (no third-party round trip) and computes the fallback font metrics (size-adjust and friends) for you:

With this setup the fallback font occupies almost exactly the same space as the real one, so the swap is visually near-invisible and contributes almost nothing to CLS.

How do you measure Core Web Vitals?

Two kinds of data, and you need to know which one you're looking at:

  • Field data is from real users. This is what Google ranks you on. Sources: PageSpeed Insights (shows CrUX data for your URL if it has enough traffic), Search Console's Core Web Vitals report, or your own analytics via the web-vitals library.
  • Lab data is a simulated load on a throttled connection, from Lighthouse or the DevTools Performance panel. Great for debugging and catching regressions, but it's not the score. Note that lab tools can't fully measure INP, since it needs real interactions; Lighthouse uses Total Blocking Time as a rough proxy.

Collecting field data yourself takes a few lines:

In Next.js, the same thing is built in as a hook:

Render it once in your root layout and every vital gets reported. (If you deploy on Vercel, the Speed Insights integration collects the same field data with zero code.)

A workflow that works: check field data monthly to know whether you have a problem and on which pages, then use lab tools to find why, fix it, and confirm the field numbers move over the following weeks.

Which metric should you fix first?

If more than one vital is failing, prioritize in this order:

  1. LCP, because it gates everything: users who leave during a slow load never experience your INP or CLS, and LCP correlates most directly with conversion and bounce.
  2. INP, because it shapes the whole session: a page that loads fast but freezes on every click feels broken.
  3. CLS, because its fixes are usually the cheapest (dimensions, reserved space, font tuning), so honestly you might knock it out first in an afternoon even if it matters least.

And once everything is green: stop. Green-to-greener is rarely the best use of your time.

FAQ

Are Core Web Vitals a Google ranking factor? Yes, they're part of Google's page experience signals. The effect is modest compared to content relevance, but between comparable pages, better vitals help. The bigger payoff is usually the user-side one: better conversion and lower bounce.

What is a good Core Web Vitals score? LCP under 2.5 seconds, INP under 200 milliseconds, and CLS under 0.1, each measured at the 75th percentile of real users. All three must be green for the page to pass.

What happened to FID? INP replaced First Input Delay in March 2024. FID only measured the input delay of the first interaction; INP measures the full input-to-paint time across all interactions, which makes it a much stricter and more realistic responsiveness metric.

Why is my Lighthouse score good but my field data bad? Lighthouse is a lab simulation of one load, often on hardware and network conditions kinder than your real audience's. Field data is your actual users at the 75th percentile. Trust the field data; use the lab to debug it.

Do Core Web Vitals apply to single-page applications? Yes, with a caveat: the metrics are measured on the initial page load, and soft navigations within an SPA aren't fully captured yet. A fast-feeling SPA can still show a poor LCP because its first load ships a large bundle before rendering anything.

Does Next.js handle Core Web Vitals automatically? It handles a lot by default: server rendering helps LCP, next/image prevents unsized-image CLS, and next/font prevents font-swap CLS. But it can't save you from a slow data fetch blocking your hero, an oversized client bundle hurting INP, or a missing priority on the LCP image. The primitives help only where you use them.

Summary

Core Web Vitals are three user experiences, made measurable:

  • LCP (under 2.5s) is "how fast did the main thing show up." Fix it with a CDN, optimized and prioritized hero images, inlined critical CSS, deferred scripts, and non-blocking fonts.
  • INP (under 200ms) is "did the page react when I touched it." Fix it by keeping the main thread free: chunk long tasks, give instant feedback before heavy work, offload to workers, keep the DOM small.
  • CLS (under 0.1) is "did things jump around." Fix it by reserving space: image dimensions, minimum heights for dynamic slots, metric-tuned font fallbacks, and transform animations.

Measure with real-user field data, debug with lab tools, fix the red and yellow zones, and stop at green.