The Core Web Vitals
Google's Core Web Vitals, the three field metrics that score your app's real-user performance and decide where it ranks in Search.
Your app is in production, real traffic is flowing through it, and Speed Insights is already streaming numbers back from real users’ phones. But a number with no model behind it is just noise. A red LCP pill on the dashboard means nothing until you know that LCP is the wait for the main content to appear, that Google scores it for Search ranking, and that the fix is usually one image. You wired the dashboard up in the last chapter; this lesson gives you the model that lets you read it.
By the end you’ll be able to look at any of the three numbers Google grades you on, name exactly what’s wrong from the metric alone, and say which lesson in this chapter fixes it. This is a map of the chapter, not a how-to: each fix is its own lesson ahead, so we’ll write almost no code here.
Three numbers Google scores you on
Section titled “Three numbers Google scores you on”Of all the things you could measure about a web page, Google has decided that three of them are the ones that predict whether a user is having a good time. They’re called the Core Web Vitals, and they’re the metrics that 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 one names a specific frustration you already know as a user: the page that takes forever to show anything, the button that doesn’t react when you press it, the article that shoves itself down the moment you start reading. We’ll define each one properly in its own section. First, though, we need the rule that decides whether a number is good or bad, because it applies to all three and you’ll lean on it throughout the lesson.
The 75th-percentile rule
Section titled “The 75th-percentile rule”Your Vitals are not scored at the average of your users. They’re scored at the 75th percentile, written p75 , of your real production traffic. Read it like this: “75% of your users get at least this experience; the other 25% get something worse.”
Why p75 and not a simple average? Because averages hide the tail. Imagine half your users are on a fast office connection and load in half a second, and the other half are on a phone with two bars of signal in an elevator and wait four seconds. The average is a comfortable 2.25 seconds, and it’s a lie, because a quarter of the people trying to use your product are having a miserable time. The p75 doesn’t let you average that pain away; it forces you to look at the user near the back of the line.
This is the answer to the most common beginner sentence in performance work: “but it’s fast on my machine.” Of course it is. You’re on a fast laptop, on fast Wi-Fi, with a warm cache, sitting next to the server. You are not the 75th percentile. The 75th percentile is a mid-range phone on a flaky mobile network, and that’s whose experience Google scores.
A trailing window, not a live alarm
Section titled “A trailing window, not a live alarm”There’s a second thing about the field score that trips people up: it doesn’t update in real time.
The public score Google Search uses comes from the CrUX dataset, which aggregates real-Chrome-user data over a rolling 28-day window. Speed Insights mirrors a recent-window p75 of your own traffic. Either way, the figure you’re looking at is a trend over weeks of real users, not a reading of the last hour.
Hold onto this, because it has a consequence we’ll return to at the end of the lesson: a performance regression you ship today will not move the public score for a week or two. The field number is a slow, honest verdict, not a same-day alarm.
The three thresholds
Section titled “The three thresholds”Each Vital is graded into three bands: good, needs improvement, and poor. You’re aiming to keep all three in the green at p75. Here are the “good” thresholds, 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: it’s a score, not a time, which makes more sense once you’ve seen how it’s calculated. Speed Insights surfaces all three of your metrics against exactly these bands, so a glance tells you which are green and which need work.
The following diagram is worth fixing in your memory. It’s the whole scoring system on one screen.
From here, there’s one section per Vital. Each section adds three things to what you already know: what the metric really measures, what one cause dominates it, and the one structural change that moves it.
LCP: the wait for the main thing to appear
Section titled “LCP: the wait for the main thing to appear”LCP measures the render time of the largest content element visible in the initial viewport. On most pages that’s the hero image, a big headline, or the first card in a list, the thing the page is about. Put plainly: how long until the main thing shows up.
The key thing to get right about LCP is that it tracks one element, not the whole page. It is not “fully loaded.” It is not when the spinner stops or the last analytics script finishes. It’s the moment that single biggest element paints, and nothing else. The illustration below points at a typical LCP candidate: on a marketing page it’s almost always the hero image.
Why the network owns LCP
Section titled “Why the network owns LCP”To see why LCP behaves the way it does, walk it against the render pipeline you already know: the browser parses HTML into the DOM, parses CSS into the CSSOM, runs layout, then paints. For the LCP element specifically, four things have to happen in order: the HTML has to arrive, the browser has to discover the LCP element inside that HTML, fetch its bytes, and finally paint it.
The slow step is almost always byte delivery. A large hero image is hundreds of kilobytes that have to travel down a real, often mobile, connection. A web font that the headline depends on can’t paint text until the font file arrives. Either way the bottleneck is the critical path , and that’s a network problem, not a CPU one.
What goes wrong, and how you fix it
Section titled “What goes wrong, and how you fix it”In a Next.js SaaS, a poor LCP almost always traces to one of three things:
- The hero image was shipped without telling the browser it’s important, so the browser discovers it late, only after layout, and starts fetching it far too slowly.
- The headline depends on a custom font that wasn’t loaded through
next/font, so the text can’t paint until the font arrives. - A Server Component is awaiting a slow upstream, such as a database query or an API call, before it can render the element at all.
Each of those has a fix, and each fix is its own lesson, so here we just name them:
- Mark the LCP image as high priority so the browser fetches it immediately. That’s the next lesson, on marking the hero image high-priority.
- Load the primary typeface through
next/font, which you already met when we covered fonts. The font ships with the page instead of being discovered late. - Get the slow data fetch off the critical path so it doesn’t block the first paint. That’s the lesson on Server Component waterfalls later in this chapter.
Notice that LCP has three levers where the other two Vitals have one. That’s not an accident: LCP sits exactly where the network, your fonts, and your server timing all meet, so this chapter spends more lessons on it than on anything else.
INP: does the page respond when you tap
Section titled “INP: does the page respond when you tap”INP measures the latency of your interactions across the whole visit: from the moment you tap, click, or press a key, to the next frame the browser paints in response. Put plainly: you tapped, so how long until something happened?
That’s the plain version; here’s the precision. INP doesn’t report your average interaction. It reports roughly the worst one of the visit (technically near the p98 of the page’s interactions). So a page can feel snappy for nineteen taps and earn a poor INP from the twentieth. One janky interaction sets the score, which is why a single heavy handler can quietly drag down a whole page.
Why the main thread is the bottleneck
Section titled “Why the main thread is the bottleneck”Here’s the fact that explains INP: the browser runs your JavaScript and paints the screen on one and the same thread, the main thread . The two take turns.
So when you tap a button, the browser wants to run your click handler and then paint the result, but if the main thread is already busy, your tap waits in line.
And it gets busy easily: a heavy Client Component re-rendering its whole 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 your input or paint a response.
The page looks frozen because, for that instant, it is.
INP replaced FID, and why that matters
Section titled “INP replaced FID, and why that matters”Here’s something you’ll trip over the moment you read anything older than 2024. Until March 2024, the interactivity Vital was FID . FID only measured the first interaction, and only the input delay part of it, not how long your handler took or how long the paint took.
INP fixed both gaps: it measures every interaction across the visit and includes the processing and rendering time, not just the wait to start. That makes it a far more honest reflection of what the user actually feels.
So here’s the practical rule: if a tutorial, a dashboard, or a Stack Overflow answer talks about FID, it’s stale. The metric you care about is INP.
How you fix it
Section titled “How you fix it”The structural fix for INP is the one this whole course has been quietly setting you up for: ship less JavaScript to the client.
Every line of client JavaScript is something that can land on the main thread and block a tap.
The Server Components you’ve been writing do their work on the server and send down finished HTML, at no client cost.
So the discipline is to 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 it onto the client.
When a single interactive widget is unavoidably heavy, there are further moves: code-splitting it with dynamic(), debouncing a high-frequency handler, or pushing heavy synchronous work to a Web Worker. But those are conditional tools, not the default.
The next two lessons are about finding and cutting client JavaScript weight, which is the real lever here.
When you need to see which interaction is slow and why, the Chrome DevTools Performance panel, the one you met earlier in the course, has an INP overlay that points straight at the offending interaction.
CLS: does the page jump while you read
Section titled “CLS: does the page jump while you read”CLS measures the cumulative score of unexpected layout shifts over the page’s lifetime, meaning content that moves after it was already painted.
Picture it. You open an article, you go to tap a button, and in the half-second before your finger lands an image finishes loading above the button, the whole page shoves down, and you tap an ad instead. That jump is a layout shift, and it’s exactly what CLS scores. The “unexpected” matters: a shift you caused by tapping a “show more” button doesn’t count, but a shift that happens to you while you’re reading does.
How the score adds up
Section titled “How the score adds up”You won’t compute CLS by hand, so we’ll keep this qualitative. Each individual shift is scored by two factors multiplied together: how much of the screen moved (the impact), and how far it moved (the distance). A small element nudging a few pixels barely registers, while a big block jumping half the screen scores badly. The page’s CLS is the sum of its worst shift windows, so a single big jump and a steady drip of little ones both add up against you.
The takeaway is the intuition, not the arithmetic: a bigger thing moving farther scores worse.
What causes it, and how you fix it
Section titled “What causes it, and how you fix it”Layout shifts almost always come from the same family of mistakes: 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 it down.
- A banner, toast, or cookie bar injected late, shoving the content that was already there.
- A font swap. The fallback text paints, then the web font loads at a slightly different size and the text reflows .
The structural fix is a single idea: reserve the space for everything dynamic before it arrives.
next/imagerequires you to passwidthandheight(or a sizedfillcontainer). That requirement isn’t bureaucracy: the reservation it forces is the CLS fix. (Worth flagging now: the blur placeholder you might add to an image is UX polish, while the dimensions are what actually prevent the shift. We’ll come back to that next lesson.)next/fontships fallback font metrics tuned so the swap from fallback to web font doesn’t change the text’s size, which means no reflow.- A loading skeleton has to match the dimensions of the real content it stands in for, or you’ve just moved the shift to the moment the content replaces the skeleton.
- Modals and toasts should overlay the page with
position: fixedso they sit on top of the flow instead of displacing it.
The following diagram shows the difference between a shift and a reservation directly. The first tab loads an image with no reserved box, and the headline and button get shoved down. The second loads the same image into a pre-sized box, and nothing moves.
Why it’s not cosmetic
Section titled “Why it’s not cosmetic”It’s tempting to file CLS under “annoying but harmless,” but don’t. A page that jumps mis-fires conversion clicks: the user reaches for “Buy” and hits “Cancel,” or taps an ad they never wanted, and that’s lost revenue and lost trust in the same gesture. A page that visibly lurches around also just reads as broken and amateur, the way a wobbly table reads as a cheap restaurant.
There’s one more reason to take it seriously. Of the three Vitals, CLS is the cheapest to fix structurally, since mostly it’s just declaring dimensions you should have declared anyway, and the most conspicuous to leave broken. There’s no good reason to ship it.
Field data is the verdict, lab data is the regression catcher
Section titled “Field data is the verdict, lab data is the regression catcher”We’ve talked about the metrics. Now for the rule that governs how you act on them, the discipline that the rest of this chapter relies on.
There are two completely different ways to measure your Vitals, and confusing them is the classic beginner mistake.
Field data is what you’ve been reading from Speed Insights: real users, on real devices, over real networks, aggregated into that 28-day p75. This is what Google Search actually scores you on. It is the verdict, the truth about whether your users are suffering.
Lab data comes from a tool like Lighthouse (which you’ll set up later in this chapter): one synthetic run in a controlled, simulated environment, with a throttled slow network, a mid-tier mobile CPU, no browser extensions, and no cache. It’s reproducible, it’s instant, and crucially you can run it before you deploy. It is the regression catcher, not the verdict.
The rule that ties them together:
Chase the field numbers. Use the lab numbers to stop regressions before they ship.
And when the two disagree, which they will, field wins for deciding what to fix, and lab wins for catching a regression early. They answer different questions at different times: field data is continuous and after-the-fact (and lags by weeks), while lab data is on-demand and pre-deploy. That’s exactly why you need both, since neither one covers the other’s timing.
Here’s the trap, stated plainly so you recognize it in yourself. A beginner discovers Lighthouse, sees a score out of 100, and starts chasing a perfect 100 for that one synthetic profile, tuning the app for a simulated mid-range phone that no actual human owns, while the field data quietly shows real mobile users on flaky networks still hurting. Never chase a lab score while field data says otherwise. The synthetic 100 is a vanity metric; the field p75 is your users.
Try the following exercise to lock the distinction in. Each chip describes a job; drop it under the surface that does it. This is the exact split the rest of the chapter leans on, so it’s worth knowing cold.
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.
TTFB and FCP: the upstream tells
Section titled “TTFB and FCP: the upstream tells”Speed Insights reports two more numbers next to the three Vitals. You should know what they are so you don’t mistake them for Vitals, and so you can read them as clues when a Vital goes red.
These two are not Core Web Vitals. But they’re the diagnostic breadcrumbs that tell you where to look when LCP is poor.
TTFB is how long it takes the server to send the very first byte of HTML after the request goes out. If TTFB is high, your server is slow to respond, perhaps because of a stack of sequential awaits or a slow database query, and everything downstream, including LCP, inherits that delay before the browser has even seen a single byte.
FCP is when any content first paints: the first text or image of any kind, not necessarily the big one. Read it in relation to the others:
- If FCP is high, the browser was blocked from painting anything early on, usually by a render-blocking stylesheet or script, or by a slow TTFB dragging the whole timeline.
- If FCP is fine but LCP lands much later, then the page started painting on time and it’s the LCP element specifically, its image or font, that’s the holdup.
That gives you a tidy reading order whenever LCP is red: check TTFB first (is the server slow?), then FCP (did rendering start late?), then the LCP element itself (is it just that one image?). Three checks, in that order.
The point of these two metrics is that they turn “the number is red” into “here’s where to look,” which is the entire job of a vigilance chapter. The following strip shows where each one sits on a single page-load timeline, so it’s obvious why a slow TTFB drags LCP along behind it.
What runs once, what runs forever
Section titled “What runs once, what runs forever”Remember the 28-day window from the start of the lesson? Now we can use it, because it dictates when you do performance work, and that’s the whole premise of this chapter.
Because field data is a trailing 28-day average, it’s a stability feature, not a same-day alarm. You cannot lean on Speed Insights to tell you that a deploy broke performance: by the time the score visibly moves, the regression has been live and hurting users for a week or two. The rolling window is for spotting a trend, not for triage.
So regression detection has to happen before the deploy goes out, as a lab check in your pipeline, which is the Lighthouse lesson coming up. The field dashboard confirms the trend; the pre-deploy gate catches the regression. Those are the two rhythms of performance vigilance, and they’re the spine of this whole chapter:
- A pre-launch deep pass, done once. Before you ship, you audit your highest-traffic pages, find what’s slow, and fix it structurally.
- Recurring vigilance: the lab gate that runs on every pull request, and the weekly glance at your slowest database queries. Small, repeated checks that keep the app fast as it grows.
That’s the thesis of the chapter in one line: a small set of recurring checks plus a handful of structural defaults you set once and never undo.
Which brings us to the map. You can now look at any Vital and name what’s wrong; here’s where each fix lives in the lessons ahead.
The metric tells you what hurts. The rest of this chapter is how to fix each one. You now have the map; next we make the LCP image fast.
External resources
Section titled “External resources”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.
Google's canonical definitions of LCP, INP, and CLS with the current good thresholds — the source of truth to re-check if the numbers ever drift.
The authoritative statement of what INP measures and why it replaced FID in March 2024.
Vercel's own walkthrough of the triad and the field-vs-lab split, wired to the exact Speed Insights stack this chapter uses.
Paste any URL and watch its layout shifts animate frame by frame — an interactive way to see CLS instead of just reading about it.