Skip to content
Chapter 94Lesson 1

The Core Web Vitals

Google's Core Web Vitals, the three field metrics that score real-user performance and feed Search ranking.

Your app is in production and Speed Insights is streaming numbers from real users’ phones. But a red LCP pill is just noise until you know what LCP measures, why Google scores it, and what usually fixes it.

This lesson builds that model for the three numbers Google grades you on, so one metric tells you what is wrong and which lesson in this chapter fixes it.

The three Core Web Vitals and how they’re scored

Section titled “The three Core Web Vitals and how they’re scored”

Google grades three numbers that predict whether a user is having a good time, and they feed Google Search ranking.

  • LCP : how long until the main thing on the page appears.
  • INP : when you tap something, how long until the page responds.
  • CLS : how much the page jumps around while you’re trying to read it.

Each names a frustration you already know: the page that shows nothing, the button that ignores your press, the article that shoves itself down as you read. Each gets its own section below; first, the rule that grades all three.

Vitals are scored at the 75th percentile (p75) of your production traffic, not the average: 75% of users get at least this experience, and the other 25% get something worse.

Why p75? Because averages hide the tail. If half your users load in half a second and half wait four seconds on a weak signal, the average is a comfortable 2.25 seconds, and a lie: a quarter of your users are miserable. The p75 won’t let you average that pain away; it points at the user near the back of the line.

This answers “but it’s fast on my machine.” You have a fast laptop, fast Wi-Fi, and a warm cache; Google scores the mid-range phone on a flaky network.

The public score Google Search uses comes from CrUX (the Chrome User Experience Report), a rolling 28-day window of real Chrome-user field data; Speed Insights mirrors a recent-window p75 of your own traffic. Either way the figure is a trend over weeks, not the last hour, so a regression you ship today won’t move the public score for a week or two.

Each Vital is graded good, needs improvement, or poor, and you want all three green at p75. The “good” thresholds are the only numbers in this lesson worth memorizing:

  • LCP: good at ≤ 2.5 seconds (poor above 4.0s).
  • INP: good at ≤ 200 milliseconds (poor above 500ms).
  • CLS: good at ≤ 0.1 (poor above 0.25).

CLS is unitless, a score rather than a time, which makes more sense once you’ve seen how it’s calculated. Speed Insights plots all three against these bands, so a glance shows which need work.

good
needs improvement
poor
LCPseconds
≤ 2.5s
to 4.0s
> 4.0s
INPmilliseconds
≤ 200ms
to 500ms
> 500ms
CLSscore (unitless)
≤ 0.1
to 0.25
> 0.25

The sections below add, for each Vital, what it really measures, the one cause that dominates it, and the structural change that moves it.

LCP measures the render time of the largest content element visible in the initial viewport, usually the hero image, a big headline, or the first card in a list, the thing the page is about. In short: how long until the main thing shows up.

LCP tracks one element, not the whole page. Not “fully loaded,” not when the spinner stops or the last analytics script finishes, just the moment that single biggest element paints. On a marketing page that’s almost always the hero image, as in the figure below.

this element’s paint time is your LCP

Walk the LCP element through the render pipeline you already know. Four things happen in order: the HTML arrives, the browser discovers the element inside it, fetches its bytes, and paints it.

The slow step is almost always byte delivery. A hero image is hundreds of kilobytes traveling down a real, often mobile, connection, and a headline in a custom font can’t paint until the font file arrives. Either way the bottleneck is the critical path , a network problem, not a CPU one.

In a Next.js web app, a poor LCP almost always traces to one of three things, each with its own fix:

  • The hero image shipped without a priority hint, so the browser discovers and fetches it too late. Mark it high priority so the browser fetches it immediately; that’s the next lesson.
  • The headline waits on a custom font not loaded through next/font. Load the typeface through next/font and it ships with the page instead of being discovered late.
  • A Server Component awaits slow upstream data, like a database query, before it can render the element. Get that fetch off the critical path so it doesn’t block first paint; that’s the Server Component waterfalls lesson later in this chapter.

LCP has three levers where the other two Vitals have one, because it sits exactly where the network, your fonts, and your server timing meet.

INP measures interaction latency across the whole visit: from the moment you tap, click, or press a key, to the next frame the browser paints in response.

It reports not your average interaction but roughly the worst, technically near the p98 of the page’s interactions. A page can feel snappy for nineteen taps and still earn a poor INP from the twentieth, so one heavy handler can quietly drag down the score.

The browser runs your JavaScript and paints the screen on one and the same thread, the main thread , and the two take turns.

So when you tap a button, the browser wants to run your click handler and then paint, but if the thread is already busy, your tap waits in line. And it gets busy easily: a heavy Client Component re-rendering its subtree, a synchronous JSON.parse chewing through a large payload, a third-party analytics script hogging the event loop. While any of that runs, the thread can’t process input or paint, and the page looks frozen because for that instant it is.

Until March 2024, the interactivity Vital was FID (First Input Delay), which measured only the first interaction and only the wait before its handler started, not how long the handler or paint took. INP closes both gaps: it measures every interaction and includes processing and rendering time, so it reflects what the user actually feels. If a tutorial or dashboard still talks about FID, it’s stale; the metric you care about is INP.

The structural fix for INP is to ship less JavaScript to the client.

Every line of client JavaScript can land on the main thread and block a tap. A Server Component does its work on the server and sends down finished HTML at no client cost, so let the server do the work and put 'use client' only on the genuine interactive leaves of the tree, not high up where it drags everything below onto the client.

When a single widget is unavoidably heavy, you have further moves: code-splitting it with dynamic(), debouncing a high-frequency handler, or pushing heavy synchronous work to a Web Worker. The next two lessons cover finding and cutting client JavaScript weight, the real lever here. To see which interaction is slow and why, the Chrome DevTools Performance panel has an INP overlay that points straight at the offending interaction.

CLS: how much the page jumps while you read

Section titled “CLS: how much the page jumps while you read”

CLS scores unexpected layout shifts over the page’s lifetime: content that moves after it was already painted.

Picture opening an article and reaching to tap a button. In the half-second before your finger lands, an image loads above the button, the page shoves down, and you tap an ad instead. That jump is a layout shift, and it’s what CLS scores. The “unexpected” part matters: a shift you triggered by tapping “show more” doesn’t count, but one that happens to you while you read does.

You’ll never compute CLS by hand, so hold the intuition, not the arithmetic. Each shift multiplies two factors: how much of the screen moved (the impact) and how far it moved (the distance). A small element nudging a few pixels barely registers; a big block jumping half the screen scores badly. The page sums its worst shift windows, so one big jump and a steady drip of little ones both count against you.

Layout shifts almost always trace to one mistake: content that arrives without its space reserved.

  • An image with no declared dimensions. The browser leaves a zero-height box, then expands it when the bytes arrive, pushing everything below down.
  • A banner, toast, or cookie bar injected late, shoving aside content already there.
  • A font swap. The fallback text paints, then the web font loads at a different size and the text reflows .

The fix is one idea: reserve the space for everything dynamic before it arrives.

  • next/image requires width and height (or a sized fill container). The reservation it forces is the CLS fix; the blur placeholder you might add is UX polish, not the fix. (More next lesson.)
  • next/font ships fallback metrics tuned so the swap to the web font doesn’t change the text’s size, so nothing reflows.
  • A loading skeleton must match the real content’s dimensions, or you’ve just moved the shift to the moment it’s replaced.
  • Modals and toasts should overlay with position: fixed so they sit on top of the flow instead of displacing it.

The diagram contrasts the two directly: the same image load, with and without a pre-sized box.

Buy now headline + button jump down
The image loads into an unsized box, so the headline and button get shoved down.

CLS isn’t harmless. A page that jumps mis-fires clicks: the user reaches for “Buy” and hits “Cancel,” or taps an ad they never wanted, losing you revenue and trust in one gesture. A page that visibly lurches also reads as broken and amateur.

Of the three Vitals, CLS is the cheapest to fix, since it mostly means declaring dimensions you should have declared anyway, and the most conspicuous to leave broken.

Field data scores you, lab data catches regressions

Section titled “Field data scores you, lab data catches regressions”

Vitals are measured two ways, and confusing them is the classic beginner mistake.

Field data is what Speed Insights shows: real users on real devices and networks, aggregated into the 28-day p75. It’s what Google Search scores you on, the verdict on whether users are suffering.

Lab data comes from a tool like Lighthouse: one synthetic run with a throttled network, a mid-tier mobile CPU, no extensions, no cache. Reproducible, instant, and runnable before you deploy, it catches regressions but is never the verdict.

The rule:

Chase the field numbers. Use the lab numbers to stop regressions before they ship.

The trap: a beginner finds Lighthouse, sees a score out of 100, and chases a perfect 100 for one synthetic profile, tuning for a simulated phone no one owns while real users on flaky networks keep hurting. The synthetic 100 is a vanity metric; the field p75 is your users.

Field Speed Insights the verdict
Lab Lighthouse the regression catcher
Source
Real users, real devices, real networks
One synthetic run — simulated slow network + mid-tier mobile CPU
Timing
Continuous, lags ~28 days
Instant, on demand, pre-deploy
Job
The verdict — what Google Search scores
The regression catcher — before you ship

Lock in the distinction with the exercise below.

Each chip describes a job. Drop it under the surface that does it. Drag each item into the bucket it belongs to, then press Check.

Field data — Speed Insights Real users, 28-day p75
Lab data — Lighthouse One synthetic run, pre-deploy
Decides where you rank in Google Search
Averages four weeks of actual visitor sessions
The honest answer to whether people are suffering
Fires in your pipeline before a deploy goes out
A single throttled mid-range-phone simulation
Flags a slowdown inside a pull request

TTFB and FCP: where to look when LCP is slow

Section titled “TTFB and FCP: where to look when LCP is slow”

Speed Insights reports two more numbers beside the three Vitals. Neither is a Core Web Vital, so treat them not as goals but as clues for where to look when LCP goes red.

TTFB is how long the server takes to send the first byte of HTML after the request goes out. A high TTFB, often a stack of sequential awaits or a slow query, delays everything downstream before the browser sees a single byte.

FCP is when any content first paints: the first text or image, not necessarily the big one. Read it against the others:

  • A high FCP means something blocked the browser from painting early, usually a render-blocking stylesheet or script, or a slow TTFB.
  • An on-time FCP with a much later LCP means painting started fine and the holdup is the LCP element itself, its image or font.

So whenever LCP is red, read in order: TTFB (slow server?), FCP (rendering started late?), then the LCP element (just that one image?).

request t = 0
TTFB first byte
FCP first paint
LCP main element
request → TTFB server time covered later — waterfalls; DB queries
TTFB → FCP render start render-blocking resources
FCP → LCP the LCP element itself image priority — next lesson
TTFB and FCP aren't Vitals; they're the upstream stretches of the same timeline that ends in LCP.

The 28-day lag tells you when performance work has to happen. By the time a regression visibly moves the field score, it has been live and hurting users for a week or two. So you cannot catch regressions in the field; you catch them in the lab, with a check that runs before the deploy, the pre-deploy gate of the Lighthouse lesson coming up.

That gives the chapter its two rhythms of performance vigilance:

  • A pre-launch deep pass, done once: before you ship, audit your highest-traffic pages, find what’s slow, and fix it structurally.
  • Recurring vigilance: the lab gate on every pull request, plus a weekly glance at your slowest database queries. Small, repeated checks that keep the app fast as it grows.

That is the thesis in one line: a few recurring checks, plus a handful of structural defaults you set once and never undo.

You can now look at any Vital and name what’s wrong. Here is where each fix lives in the lessons ahead.

Vital
Primary cause
Structural fix
Where in this chapter
LCP
Hero image discovered late, a blocking font, or a slow server
Mark the LCP image as priority
Next lesson — priority on the LCP element
Sequential awaits in a Server Component
Parallelize the independent fetches
The RSC-waterfalls lesson
A missing index or an N+1 at the database
Composite indexes, batch the queries
The indexes-and-N+1 lesson
INP
Too much main-thread JavaScript
Ship less client JS — let the server do the work
The bundle lessons — barrel-export trap, then the treemap
CLS
Unsized media and late-injected content
Reserve space for everything dynamic
No dedicated lesson — set the defaults from this one
All three A regression slips through before deploy — add a lab gate that runs before shipping. The Lighthouse lesson guards LCP, INP, and CLS in one pass.

Next we make the LCP image fast.

The thresholds in this lesson are Google’s, and Google has tightened them before, so the canonical place to confirm the current numbers is web.dev’s own pages.