Core Web Vitals
Three numbers, measured on real users, that say whether a page loads fast (LCP), reacts fast (INP), and stays still (CLS): what each one measures, what makes it bad, and how to fix it.
Why Core Web Vitals exist
Picture a news site. The team runs a speed test on a fast office laptop and gets a great score. The same week, support tickets say the site "feels broken" on phones.
Readers tap the menu and nothing happens for half a second. The article text jumps down just as they start to read, because an ad loaded above it. The hero photo shows up three seconds after everything else.
Nobody on the team was lying. They were measuring the wrong thing, in the wrong place. "Page load time", the old favourite, is the moment the load event fires: when the document and all its images, scripts, and stylesheets have finished loading. It says nothing about when the user saw the main content, whether the page answered their taps, or whether things moved under their finger.
Core Web Vitals are Google's answer to that gap: three metrics, each tied to one thing a user feels, each with a clear "good" line, and each measured on real visits rather than on a developer's laptop.
Three questions a user asks
Every visit, a user silently asks three questions. Each Core Web Vital turns one question into a number you can track.
| The user asks | Metric | What it measures | Good at the 75th percentile |
|---|---|---|---|
| Is the page here yet? | LCP, Largest Contentful Paint | When the biggest image or text block in the viewport was painted | 2.5 s or less |
| Does it react when I tap? | INP, Interaction to Next Paint | How long the page takes to show a response after a click, tap, or key press | 200 ms or less |
| Does it stay still? | CLS, Cumulative Layout Shift | How much visible content moves without the user asking it to | 0.1 or less |
The right column says "at the 75th percentile". A page does not pass because it is fast for you: it passes when at least three out of four real visits are under the line. A later chapter explains why that is the right rule.
Why the whole industry tracks them
Google uses Core Web Vitals as one of the signals in search ranking (the "page experience" signals). It is a small signal next to relevance, but it made every large site measure the same three things the same way. That shared vocabulary is the real win: a product manager, a backend engineer, and an SEO team can read the same dashboard.
Later lessons in this module cover the fixes in depth: loading strategies, images, fonts, runtime performance, and budgets. This lesson is the map that tells you which fix a given number is asking for.
The mental model: one visit, three moments
Keep one picture in your head: a single visit drawn as a line from the moment the user taps a link to the moment they leave. Each vital looks at a different stretch of that line and keeps its own kind of number.
- LCP is a moment early in the visit. It is a time in seconds, measured from the start of the navigation. It is decided once, during loading, and stops updating as soon as the user taps, clicks, scrolls, or presses a key.
- INP is about the whole visit. It is a duration in milliseconds: roughly the slowest response to any tap, click, or key press, from the first to the last. A page that loads fast but freezes on the third click still fails.
- CLS is about the whole visit too. It is a score with no unit, built from every unexpected movement of visible content. A shift ten minutes in, when an infinite list loads a banner, counts just like a shift during loading.
This split tells you where to look when a number is bad. A bad LCP is usually a loading problem: the network, the server, or the order in which the browser fetches things. A bad INP is usually a main thread problem: too much JavaScript at the wrong time. A bad CLS is usually a layout problem: no space reserved for something that arrived late.
An everyday comparison
Think of a restaurant. LCP is how long until your main dish arrives, INP is how quickly a waiter responds when you raise your hand (judged on your slowest wait, not the first), and CLS is whether someone keeps moving your plate while you eat. A restaurant can be great at one and awful at another, and guests remember the worst, so all three have to be good.
Three bands for each metric
Each metric has two lines that split visits into three bands: good, needs improvement, and poor. A score is "good" at or below the first line, and "poor" above the second.
| Metric | Good | Needs improvement | Poor |
|---|---|---|---|
| LCP | 0 to 2.5 s | over 2.5 s up to 4 s | over 4 s |
| INP | 0 to 200 ms | over 200 ms up to 500 ms | over 500 ms |
| CLS | 0 to 0.1 | over 0.1 up to 0.25 | over 0.25 |
| TTFB (helper) | 0 to 0.8 s | over 0.8 s up to 1.8 s | over 1.8 s |
| FCP (helper) | 0 to 1.8 s | over 1.8 s up to 3 s | over 3 s |
When does a page pass?
A page passes the Core Web Vitals assessment when all three metrics are good at the 75th percentile of real visits, measured separately for phones and for desktops. Two out of three is a fail. A great desktop score does not cover a poor mobile score: tools like PageSpeed Insights and Search Console show the two separately.
Check your understanding · Exercise 1
Which of these problems would show up mainly in INP? Select all that apply.
LCP: when the main content shows
LCP, Largest Contentful Paint, is the time from the start of the navigation until the largest image or text block inside the viewport is painted. It is the browser's best guess at "the main content is here": usually the product photo on a product page, the headline or first paragraph on an article, and the hero image on a landing page.
What can be the LCP element
- An
imgelement, including an image inside ansvg. For an animated image, the first frame counts. - A
videoelement: its poster image, or its first frame when there is no poster. - An element with a background image loaded through CSS
url(). - A block-level element that contains text, such as a
p, anh1, or adivwith text in it.
The size used is only the part that is visible inside the viewport. Anything below the fold, clipped by overflow, or hidden with opacity: 0 does not count. For images, the browser also takes the smaller of the displayed size and the file's real size, so stretching a tiny image to fill the screen does not make it the LCP element. Chrome also ignores images with very little detail per pixel, so a blurry placeholder does not end LCP early.
LCP is a running guess
The browser does not know in advance which element will be largest. As the page paints, it reports a new candidate each time something bigger appears: first the logo, then the headline, then the hero image. The last candidate before the user interacts is the final LCP. Once the user taps, scrolls, or presses a key, the browser stops looking, because from then on the page changes for the user's reasons.
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Each entry is a new, larger candidate. The last one wins.
const lcp = entry as LargestContentfulPaint;
console.log("LCP candidate", Math.round(lcp.startTime), lcp.element);
}
}).observe({ type: "largest-contentful-paint", buffered: true });
buffered: true replays entries that happened before the observer started.Split LCP into four parts
A single LCP number tells you that something is slow, not what. The most useful habit is to split the time into four parts that follow each other with no gaps. Each part has different causes and different fixes.
- TTFB, time to first byte. From the start of the navigation until the first byte of the HTML arrives. Redirects, DNS, the connection, and the server's work all land here.
- Resource load delay. From TTFB until the browser starts downloading the LCP image. This is the time the browser did not yet know the image existed, or knew but chose to fetch other things first.
- Resource load duration. How long the image itself takes to download. Bytes and distance land here.
- Element render delay. From the end of the download until the image or text is actually painted. Render-blocking CSS, scripts, web fonts, and client-side rendering land here.
When the LCP element is text, parts 2 and 3 are zero: there is no resource to fetch, so everything after TTFB is render delay, including any wait for a web font. web.dev suggests a healthy page spends roughly 40% of its LCP on TTFB, 40% on the image download, and under 10% each on the two delays. The delays are pure waste: the browser is waiting for no good reason.
What makes LCP bad, part by part
| Part | Typical causes | Fixes |
|---|---|---|
| TTFB | Slow server rendering, uncached HTML, redirects, a far-away origin | Cache HTML at a CDN, stream the response, remove redirect hops |
| Load delay | Image added by JavaScript, set as a CSS background, lazy-loaded, or queued behind other requests | Put a real img in the server HTML, add fetchpriority="high", preload it if it must come from CSS, never lazy-load it |
| Load duration | Large file, old format, wrong pixel size for the screen, no CDN | Right-size with srcset, use AVIF or WebP, serve from an image CDN near the user |
| Render delay | Render-blocking CSS and scripts, a web font hiding text, client-side rendering, an A/B test snippet that hides the page | Inline critical CSS, defer scripts, use font-display, render on the server, drop anti-flicker snippets |
<!-- In the server HTML, so the preload scanner finds it at once -->
<img
src="/p/shoe-800.avif"
srcset="/p/shoe-400.avif 400w, /p/shoe-800.avif 800w, /p/shoe-1200.avif 1200w"
sizes="(max-width: 600px) 100vw, 50vw"
width="800" height="800"
fetchpriority="high"
alt="Blue running shoe, side view"
/>
The browser's preload scanner is the reason the first fix works so well. While the main parser is blocked on a script, a second, lighter parser races ahead through the raw HTML and starts downloads for every img, link, and script it sees. An image that only exists after your JavaScript runs is invisible to it. The critical-rendering-path lesson walks through this in detail.
A worked example
A product page on a mid-range phone has a p75 LCP of 3.8 s. Splitting it gives: TTFB 900 ms, load delay 1,300 ms, load duration 1,000 ms, render delay 600 ms. The load delay is the biggest part, and it is pure waste. The cause: the product photo is rendered by React after a 300 KB bundle downloads and runs.
- Render the
imgin the server HTML withfetchpriority="high". Load delay drops from 1,300 ms to about 150 ms. - Serve an 800-pixel AVIF of about 90 KB instead of a 480 KB JPEG. Load duration drops from 1,000 ms to about 300 ms.
- Cache the HTML at the CDN for anonymous visitors. TTFB drops from 900 ms to about 350 ms.
- Inline the small amount of CSS above the fold and defer the bundle. Render delay drops from 600 ms to about 100 ms.
The new total is about 0.9 s, well under 2.5 s. Try the combinations in the lab below and notice which single fix moves the number most. The answer depends on which part is biggest, which is the point of splitting it.
| Part | Time | Share of LCP |
|---|---|---|
| TTFB | 900 ms | 24% |
| load delay | 1300 ms | 34% |
| load duration | 1000 ms | 26% |
| render delay | 600 ms | 16% |
Check your understanding · Exercise 2
A page's LCP element is the h1 headline. TTFB is 400 ms and LCP is 2.9 s. Which part is most likely to blame? Pick one.
TTFB and FCP: the metrics that explain LCP
Two more metrics are not Core Web Vitals, but you will read them every time LCP is bad. They are the first two marks on a page load's timeline, and the gaps between them and LCP tell you where to look.
TTFB: time to first byte
TTFB is the time from the start of the navigation until the first byte of the HTML response arrives. Good is 0.8 s or less. It is not just "server time". It is everything that happens before the browser can start parsing:
- Redirects. Each hop, such as
httptohttpsorexample.comtowww.example.com, costs a full round trip before the real request starts. - Service worker startup. If a service worker controls the page and is not running, the browser has to start it first, which can take tens of milliseconds on a phone.
- DNS lookup, TCP connection, and TLS handshake. About three round trips on a cold connection with TLS 1.3: one each for DNS, TCP, and TLS. On a 100 ms mobile round trip that alone is about 300 ms.
- The request and the server's work. Routing, database queries, rendering HTML, and any calls to other services.
- The first byte travelling back. Another half round trip.
This is why a CDN helps TTFB twice. The connection is made to an edge server a few milliseconds away instead of an origin across an ocean, and a cached HTML response skips the server's work entirely. Streaming server rendering helps too: the server sends the head and the page shell right away, so the first byte leaves before slow data is ready.
FCP: first contentful paint
FCP is the time until the browser paints anything from the DOM: any text, any image (including a CSS background image), an svg, or a non-blank canvas. Good is 1.8 s or less. FCP is the moment the user stops staring at a blank screen and knows the page is coming.
FCP waits on the critical rendering path: the HTML has to arrive, every render-blocking stylesheet in the head has to download and parse, and every classic script without async or defer before the content has to run. The critical-rendering-path lesson covers each step; here you only need the effect. A page with 300 KB of CSS in the head cannot paint anything until all of it is in.
Read the gaps, not the numbers
Put TTFB, FCP, and LCP on one line and look at the distances between them. Each gap points at a different kind of problem.
| What you see | What it usually means | Where to look |
|---|---|---|
| TTFB is already over 1 s | The server, the network, or redirects | CDN caching, server timing, redirect chains |
| Big gap from TTFB to FCP | Render-blocking CSS or scripts in the head | Inline critical CSS, defer scripts, fewer blocking requests |
| Big gap from FCP to LCP | The main content is late: an image found late, or content rendered by client JavaScript after an API call | Server-render the main content, preload or prioritize the LCP image |
| FCP and LCP almost equal | The first thing painted is the main content | Usually healthy; check TTFB and the CSS |
A client-side rendered app shows a typical pattern: an early FCP (a spinner or an empty shell painted from the HTML) and a late LCP, after the bundle downloads, runs, calls an API, and renders. The early FCP looks good on a dashboard and hides the real problem. This is why the vitals track LCP, not FCP.
Check your understanding · Exercise 3
A dashboard shows TTFB 300 ms, FCP 600 ms, LCP 3.4 s. Which changes are likely to help LCP most? Select all that apply.
INP: how fast the page responds
INP, Interaction to Next Paint, measures how long the page takes to show something after the user clicks, taps, or presses a key. It watches every such interaction during the whole visit and reports roughly the slowest one. Good is 200 ms or less; poor is over 500 ms.
"Next paint" is the key idea. The user does not care when your handler started or finished. They care when the screen changed: the button pressed in, the menu opened, the letter appeared in the box. INP ends at the first frame painted after your handlers ran.
Three phases of every interaction
- Input delay. The time from the tap until the event handlers start. The browser runs JavaScript on one main thread; if it is busy with another task, such as hydration or a third-party script, the event waits in a queue.
- Processing time. Your event handlers running, for all events in the interaction (for a tap:
pointerdown,pointerup,click). Includes any framework work they trigger, such as a re-render. - Presentation delay. The browser computing styles and layout for what changed, painting, and getting the frame to the screen. A huge DOM or a style change that touches thousands of elements makes this long.
A worked example: the user taps "Filters" 100 ms into a 300 ms hydration task. The tap waits 200 ms for that task to finish (input delay), the handler re-renders the filter panel in 80 ms (processing), and the browser takes 40 ms to lay out and paint it (presentation). The interaction's latency is 320 ms: "needs improvement", even though the handler itself was only 80 ms.
Which interaction becomes INP
Clicks, taps, and key presses count. Hovering and scrolling do not. A single tap fires several events; the interaction's latency is the longest of them. The browser times each one through the Event Timing API, a performance API that reports event durations rounded to 8 ms.
At the end of the visit, INP is the slowest interaction, with one twist: for every 50 interactions, the single slowest is ignored. A visit with 10 clicks reports the worst click; a visit with 200 key presses in an editor ignores the 4 worst and reports the fifth. That way one random hiccup on a busy page does not decide the score. A visit with no interactions at all has no INP.
Why INP replaced FID
Until 12 March 2024 the responsiveness vital was FID, First Input Delay. FID measured only the first interaction, and only its input delay. A page could pass FID with a 10 ms wait on the first click and then freeze for a second on every search keystroke. Most sites passed FID easily; many of the same sites failed INP when it arrived, because INP measures every interaction and all three phases.
What makes INP bad
- Long tasks during loading. Hydration of a big server-rendered page, a large bundle being evaluated, or tag-manager scripts all block the main thread while the user is already tapping. These show up as input delay.
- Heavy handlers. A handler that re-renders a whole page, sorts 10,000 items, or writes to
localStoragesynchronously before updating the screen. These show up as processing time. - Forced layout. Reading
offsetHeightright after changing styles, in a loop, forces the browser to lay out again and again inside the handler. - Big rendering work. A DOM of many thousands of nodes, or a style change on the
bodythat restyles everything. These show up as presentation delay.
How to fix it
Almost every INP fix is one idea: let the browser paint sooner. Do the visible part of the work first, then give the main thread back, then do the rest.
function yieldToMain(): Promise<void> {
// scheduler.yield() resumes ahead of other queued tasks where supported.
if ("scheduler" in globalThis && "yield" in scheduler) return scheduler.yield();
return new Promise((resolve) => setTimeout(resolve, 0));
}
button.addEventListener("click", async () => {
showSpinnerOnButton(); // the visible change, before the next frame
await yieldToMain(); // let the browser paint it
applyFilters(); // the heavy part, in a later task
await yieldToMain();
sendAnalytics(); // nobody is waiting for this
});
showSpinnerOnButton, not after applyFilters.- Break up long tasks. Process a big list in chunks and yield between them, so a tap can run in the gaps. Aim for tasks under 50 ms.
- Defer non-urgent work. Analytics, logging, and saving drafts can run after the paint, or in
requestIdleCallbackwhere the browser supports it. - Use the framework's tools. React's
useTransitionmarks a re-render as interruptible, so typing stays responsive while results update. - Keep the DOM small. Virtualize long lists and use
content-visibility: autofor long off-screen sections.
Try it in the simulator. The tap always lands 120 ms into a background task: make the task long, then split it into 50 ms chunks and watch the input delay collapse. The runtime-performance lesson later in this module goes much deeper on long tasks, scheduling, and profiling INP.
| Phase | Time |
|---|---|
| Input delay | 180 ms |
| Processing | 120 ms |
| Presentation | 40 ms |
| Interaction latency | 340 ms, needs improvement |
Check your understanding · Exercise 4
Typing in a search box takes 280 ms per key press to show the letter. The profile shows each input handler filtering 20,000 items and re-rendering the result list before returning. What helps? Select all that apply.
CLS: how much the page jumps
CLS, Cumulative Layout Shift, measures how much visible content moves when the user did not ask it to. You reach for "Cancel", an ad loads above it, and your finger lands on "Buy now"; or the paragraph you are reading slides down 150 pixels. Good is 0.1 or less; poor is over 0.25.
What counts as a layout shift
A layout shift happens when an element that was already visible changes its start position from one frame to the next. The browser calls these elements unstable. Some details matter:
- Adding a new element is not a shift by itself. It is the existing elements it pushes that shift.
- An element that only grows or shrinks in place, keeping its top-left corner, does not shift. The content below it does.
- Moving something with
transformdoes not count, becausetransformdoes not change layout. Animatingtop,margin, orheightdoes. - Shifts within 500 ms after a click, tap, or key press are excluded: the user expects the page to change then. Scrolling is not an input for this rule, and neither is hovering.
How one shift is scored
Each shift gets a score: impact fraction times distance fraction. The impact fraction is how much of the viewport the unstable elements touch, counting both where they were and where they ended up. The distance fraction is how far they moved, as a share of the viewport's larger side.
A worked example on a phone with a 400 by 800 viewport. An article block 400 wide and 400 tall starts 200 pixels from the top, and nothing else on screen sits below it. A 160-pixel promo banner is inserted above it and pushes it down by 160 pixels.
- Before, the block covers 200 to 600 down the screen. After, it covers 360 to 760. Together that is 200 to 760: a region 400 by 560.
- Impact fraction: 400 times 560 out of 400 times 800, which is 0.7.
- Distance fraction: 160 out of 800 (the larger side), which is 0.2.
- Shift score: 0.7 times 0.2 = 0.14. One banner, and the page is already over the 0.1 line.
Session windows: how shifts add up
The "cumulative" in CLS used to mean "add up every shift in the visit". That punished long-lived pages unfairly: a single-page app open for an hour piled up score forever. Since 2021 CLS groups shifts into session windows and reports only the worst window.
- A window starts with a shift and keeps growing while each next shift comes less than 1 second after the previous one.
- A window can last at most 5 seconds. After that, the next shift starts a new window.
- Each window's score is the sum of its shifts. CLS is the largest window score of the visit.
What makes CLS bad, and the fixes
| Cause | Why it shifts | Fix |
|---|---|---|
| Images and videos without dimensions | The browser does not know the height until the file arrives | Set width and height attributes, or CSS aspect-ratio |
| Ads, embeds, and iframes | Their size is known only after a third party responds | Reserve the slot with min-height at the most common size |
| Content injected above what is on screen | Cookie banners, "new posts" bars, promos pushed in at the top | Overlay with position: fixed, or insert below the viewport, or wait for a user action |
| Web fonts swapping in | The fallback font has different widths, so lines rewrap | Match the fallback with size-adjust and metric overrides; preload the font |
| Animating layout properties | Changing top or height each frame moves everything after it | Animate transform and opacity instead |
| Skeletons of the wrong size | The real content is taller than the placeholder | Size skeletons from real data, such as a known row height |
.ad-slot {
min-height: 250px; /* the most common ad height here */
}
.hero img {
width: 100%;
height: auto;
aspect-ratio: 16 / 9; /* space is reserved before the bytes arrive */
}
@font-face {
font-family: "Brand Fallback";
src: local("Arial");
size-adjust: 105%; /* make Arial take the same space as the web font */
}
Check your understanding · Exercise 5
Which of these add to CLS? Select all that apply.
Field data and lab data
There are two ways to get these numbers, and they answer different questions. Field data is collected from real visits on users' own devices and networks; lab data comes from a test run on a machine you control, with fixed settings. Field data tells you what is happening. Lab data helps you find out why, and check a fix before you ship it.
Where field data comes from
- CrUX, the Chrome User Experience Report. Google collects vitals from opted-in Chrome users on desktop and Android. It reports the 75th percentile over a rolling 28-day window, for whole origins and for URLs with enough traffic. PageSpeed Insights and Search Console show CrUX data, and it is the data Google Search uses.
- Your own RUM, real user monitoring. A small script on your pages measures each visit and sends the values to your backend, usually with Google's
web-vitalslibrary. You see every page and every browser that supports the APIs, and you can slice by route, release, country, or device.
import { onCLS, onINP, onLCP, onTTFB, type MetricWithAttribution } from "web-vitals/attribution";
function send(metric: MetricWithAttribution) {
const body = JSON.stringify({
name: metric.name, // "LCP", "INP", "CLS", "TTFB"
value: metric.value,
rating: metric.rating, // "good" | "needs-improvement" | "poor"
id: metric.id, // unique per page view
route: location.pathname,
release: window.__RELEASE__,
attribution: metric.attribution, // which element, which script, which phase
});
navigator.sendBeacon("/rum/vitals", body);
}
onLCP(send);
onINP(send);
onCLS(send);
onTTFB(send);
sendBeacon still delivers while the page is being hidden or closed, which is when INP and CLS report their final values.Where lab data comes from
- Lighthouse. Loads the page once with an emulated mid-range phone and a throttled network, then reports LCP, CLS during load, FCP, and TBT, Total Blocking Time: the sum of the parts of each long task over 50 ms, between FCP and the page becoming quiet. It runs in Chrome DevTools, in PageSpeed Insights, and in CI.
- The Chrome DevTools Performance panel. Shows live LCP, INP, and CLS as you use the page, and can compare them with the CrUX numbers for the same URL. Record a trace to see every task on the main thread.
Why the two disagree
A page can score 95 in Lighthouse and still fail Core Web Vitals in Search Console. The lab run is one visit under one set of conditions; the field is every visit under all of them.
- No interactions in the lab. Lighthouse never clicks anything, so it cannot measure INP. TBT is a rough stand-in: heavy blocking during load usually means slow taps during load.
- Devices and networks. Your users may be on old Android phones on a train; a lab profile is one guess at a typical device.
- Cache state. Lighthouse loads with an empty cache. Many real visits have a warm cache, or are instant Back and Forward navigations restored from memory.
- Behaviour after load. CLS from infinite scroll or a late chat widget, and a slow third click, only happen when a real person stays on the page.
- Personalization. Logged-in pages, A/B test variants, and cookie banners in some countries only exist in the field.
Decision Where should a team get its Core Web Vitals numbers?
What we pick: Use all three, for different jobs. CrUX is the scoreboard that Search uses. Your own RUM is the dashboard you act on, sliced by route and release, with attribution. Lab tests in CI are the guard rail that stops a regression from shipping. If you can only start with one, start with RUM on your top pages: it measures what users feel and tells you why.
Check your understanding · Exercise 6
Your Lighthouse LCP is 1.6 s on the product page, but CrUX shows p75 LCP of 3.2 s on phones. Which explanations are plausible? Select all that apply.
Why the 75th percentile
Every visit gets its own LCP, INP, and CLS. A page with 100,000 visits a month has 100,000 LCP values, from 0.6 s on fibre to 14 s on a weak rural signal. To judge the page you need one number, and Core Web Vitals use the 75th percentile, written p75: the value that 75% of visits are at or below.
How p75 is computed
- Collect the metric for every visit in the period, for one page or origin and one device type.
- Sort the values from fastest to slowest.
- Take the value three quarters of the way along the list. With 1,000 visits, it is the 750th value.
- Compare it with the threshold. p75 LCP of 2.3 s passes; 2.6 s does not.
Read p75 as a promise: a p75 LCP of 2.4 s means at least three out of four visits saw the main content within 2.4 s. It says nothing about the remaining quarter, which is why teams serious about performance also watch p95.
Why not the average or the median?
- The average is pulled around by a few extreme visits: one 60-second visit from a train tunnel adds as much as 60 one-second visits. Worse, a fine average can hide a large group of slow users, such as everyone on a cheap phone.
- The median, p50, only promises that half of your users are fine. A page where half the visits take 6 s can still have a good median.
- A high percentile like p99 is dominated by things you cannot fix, such as dead connections and tabs opened and immediately hidden. It would be noisy and discouraging.
p75 sits in between. It insists that most visits are good, including a fair share of slower devices and networks, while ignoring the far tail that no team can fix.
| Summary of 40 visits | LCP | Rating |
|---|---|---|
| Median (p50) | 1.9 s | good |
| Average | 2.2 s | good |
| 75th percentile (p75) | 2.1 s | good |
Play with the explorer. Move more of the audience onto slow phones and watch p75 cross the 2.5 s line long before the median moves much. This is the usual story for sites with growing traffic from emerging markets: the median looks stable while the vital quietly fails.
Segment before you judge
A p75 over "all visits" mixes populations that behave very differently. CrUX always reports phones and desktops separately, and so should your own dashboards. Other useful splits: route or page type (home, search, product, checkout), country, connection type, new versus returning visitor, and logged in versus not.
Segmenting often finds the real problem. A site whose overall p75 LCP is 2.6 s may be 1.8 s everywhere except product pages on Android, at 4.1 s. That points straight at the product image and the device class, rather than at "the site is slow".
Check your understanding · Exercise 7
A page has these LCP values from 8 phone visits: 1.2, 1.4, 1.6, 1.9, 2.2, 2.7, 3.5, 6.0 s. Which statements are true? Select all that apply.
In production: traps and real products
The definitions are simple; real sites are not. These are the situations that surprise teams most.
Single-page apps and soft navigations
In a single-page app, clicking a link changes the URL with the History API without loading a new document. The browser calls this a soft navigation. The standard metrics only know about real page loads: LCP is measured once, for the first page, and INP and CLS keep collecting across every route the user visits, all reported against the URL where the visit started.
Two effects follow. Your landing pages carry the INP and CLS of everything users do afterwards, and route changes that feel slow never show up as LCP at all. Since Chrome 151, Chromium measures soft navigations itself (the Real-user monitoring lesson shows how to report them), but other browsers don't yet, so also track route-change timing yourself in RUM (for example, from the click to the moment the new view's main content renders).
Other traps
- Anti-flicker snippets. Some A/B testing tools hide the whole page with CSS until their script picks the variant. Every visit pays that wait as LCP render delay, up to the snippet's timeout, which can be several seconds.
- Third-party tags. Tag managers, chat widgets, and ad scripts run long tasks during loading, exactly when users first tap. They are a top cause of poor INP, and they change without a deploy on your side.
- Hydration. A server-rendered page looks ready before its JavaScript has attached event handlers. Taps during hydration wait behind it, or do nothing at all. Smaller bundles, partial hydration, and islands all shorten that window.
- The back/forward cache. When the browser restores a page from memory on Back, LCP is almost instant. Pages with
unloadhandlers are often not eligible, so they lose these free fast visits. - Iframes. Layout shifts inside a cross-origin iframe, such as an embedded video or ad, count towards the page's CLS in CrUX, but your own JavaScript cannot see them. Field CLS higher than your RUM CLS is a hint.
How real products handle it
- Vodafone ran an A/B test on a landing page and published it on web.dev: a 31% better LCP came with 8% more sales. Case studies like this are why product teams fund vitals work.
- Next.js reports vitals through a
useReportWebVitalshook, and itsImagecomponent requires a size (or a fill container), so images reserve their space, and lets you mark the hero image as high priority. - Angular's
NgOptimizedImagedirective warns in development when the LCP image is not markedpriority.
A triage flow
When a vital is bad, the path from symptom to fix is short if you take it in order: which vital, which part of it, and which lesson in this module has the fix.
Check your understanding · Exercise 8
Search Console says INP is poor on your landing page, a single-page app's home route. Your RUM shows interactions on the home route itself are fast. What could explain it? Select all that apply.
Using Core Web Vitals in a system design interview
In a frontend system design interview, the non-functional requirements almost always include "it should be fast". Core Web Vitals turn that vague line into targets you can design for and measure. Say them early, then return to them when you make choices.
When to bring them up
- In the requirements. "I'll target good Core Web Vitals at p75 on mid-range phones: LCP under 2.5 s, INP under 200 ms, CLS under 0.1." This frames every later decision.
- When you choose a rendering strategy. Server rendering helps LCP; hydration cost threatens INP. Name both sides.
- When you draw the page. Point at the LCP element. Say how it is delivered and how its space is reserved.
- When you design an interaction-heavy part, such as a search box, a feed, or an editor: say how you keep each interaction under 200 ms.
- At the end, under monitoring. RUM with
web-vitals, p75 by route and device, alerts on regressions by release.
What a strong answer sounds like
For a product page: "The LCP element is the main product photo. It goes in the server HTML as a real img with fetchpriority high, a srcset of AVIF sizes, and fixed dimensions so it can't shift. The HTML is cached at the CDN for anonymous users so TTFB stays under 800 ms."
Then: "Reviews and recommendations load below the fold, in reserved space, so they don't hurt CLS. Add to cart updates the button state first and sends the request after the paint, so INP stays low even on slow phones." Each sentence ties a mechanism to a metric, which is what separates a performance answer from "we'll make it fast with lazy loading".
How deep to go, by level
| Level | What the interviewer expects |
|---|---|
| Junior | Names LCP, INP, and CLS with their thresholds, and one cause and fix for each: image size for LCP, long tasks for INP, image dimensions for CLS. |
| Mid-level | Splits LCP into its four parts and INP into its three phases; knows field from lab and why Lighthouse cannot measure INP; designs the LCP element's delivery. |
| Senior | Sets p75 targets per device class, plans RUM with attribution and release tracking, weighs rendering choices against LCP and INP together, and handles soft navigations, third parties, and regressions in CI. |
Think first
You are designing an infinite news feed with ads every 5 posts. Which vital is most at risk, and what do you design for it?
CLS is the biggest risk, and INP comes second. Ads arrive late with unknown sizes, and new posts load while the user is reading. Reserve every ad slot with a fixed min-height, append new posts below the viewport only, and never insert "new posts" content above the reading position: show a "New posts" button that the user taps instead (a shift right after a tap does not count).
For INP, a feed with thousands of posts grows the DOM until every frame is slow. Virtualize the list so only visible posts are in the DOM, and keep like and share handlers to a quick visible update followed by a deferred request.
Summary
- Core Web Vitals are three field metrics, each tied to one thing users feel: LCP for loading (good at 2.5 s or less), INP for responsiveness (200 ms or less), and CLS for visual stability (0.1 or less).
- A page passes when all three are good at the 75th percentile of real visits, judged separately for phones and desktops.
- Split LCP into TTFB, load delay, load duration, and render delay, and fix the biggest part. The two delays are pure waste.
- INP replaced FID in March 2024. It is the slowest interaction of the visit (ignoring one per 50), from input to the next paint: input delay, processing, and presentation. Paint first, then yield, then do the rest.
- A layout shift scores impact fraction times distance fraction; CLS is the worst session window of shifts. Reserve space for anything that arrives late and animate with
transform. - TTFB and FCP are not vitals, but the gaps between TTFB, FCP, and LCP tell you whether to look at the server, the render-blocking resources, or the main content.
- Field data (CrUX, your own RUM) tells you what users feel; lab data (Lighthouse, DevTools) tells you why and guards CI. Lab tests cannot measure INP.
In the interview
Questions interviewers ask about this. Answer each out loud first, then compare.
Question 1
Your product page's p75 LCP on phones is 4.1 s. Walk me through how you would fix it.
First find out what the LCP element is and split its time into four parts: TTFB, resource load delay, resource load duration, and element render delay. The web-vitals attribution build gives all four from real users, segmented by device.
Then fix the biggest part first. If TTFB is 1.2 s, cache the HTML at a CDN and remove redirects. If load delay is large because JavaScript inserts the photo, render a real img in the server HTML with fetchpriority="high". If load duration is large, serve a right-sized AVIF through an image CDN. If render delay is large, cut render-blocking CSS and scripts or move rendering to the server.
Finally, confirm with RUM by release, since CrUX's 28-day window will take weeks to fully reflect the change.
Question 2
Lighthouse gives a page 98, but Search Console says its INP is poor. How can both be true?
Lighthouse loads the page once on an emulated device and never interacts, so it cannot measure INP at all; it shows TBT as a stand-in. Real users tap during hydration or while third-party scripts run, often on slow phones, and wait behind those long tasks.
Collect INP with attribution from real users to learn which element, which event type, and which phase is slow. If input delay dominates, break up or defer the loading-time work. If processing dominates, update the screen first and yield before the heavy part. If presentation dominates, shrink the DOM or the style change.
Question 3
Why are Core Web Vitals judged at the 75th percentile rather than the average?
An average is dragged around by a few extreme visits and can hide a large slow group. The median only promises half of users are fine. A very high percentile is dominated by failures you cannot fix.
p75 asks that most visits, including a fair share of slower devices and networks, are good. It is computed separately for phones and desktops, and a page passes only when all three vitals are good at p75.
Question 4
What is the difference between FID and INP, and why did the change matter?
FID measured only the first interaction, and only its input delay: the wait before the handler started. INP, a Core Web Vital since March 2024, measures every click, tap, and key press in the visit, through all three phases up to the next paint, and reports roughly the worst.
Many sites passed FID easily because the first tap often comes after loading settles. INP exposes slow handlers, heavy re-renders, and slow later interactions that FID never saw.
Question 5
An ad slot's height is not known until the ad loads. How do you stop it from hurting CLS?
Reserve the space before the ad arrives: give the slot a min-height equal to the most common creative size for that position. If no ad fills it, keep the empty space, or collapse it only while it is below the visible area.
Avoid ad slots at the very top of content and never insert them above what the user is reading. A creative that fits inside the reserved space does not move anything around it.
Practice it
These design problems lean on core web vitals.
- Design a loading skeleton systemEasy
- Design a recently viewed products featureEasy
- Design a cookie consent managerEasy
- Design an image carouselMedium
- Design a customer support chat widgetMedium
- Design an e-commerce cart and checkoutMedium
- Design a client-side analytics SDKMedium
- Design PinterestMedium
- Design a blogging platform like MediumMedium
- Design a plan to make a slow e-commerce site fastMedium
- Design a documentation site like MDNMedium
- Design a news feedHard
- Design an A/B testing system for the frontendHard
- Design YouTubeHard
- Design an e-commerce site like AmazonHard
- Design a no-code website builderHard
- Design a 3D product configuratorHard
References
- Web Vitals (web.dev) The definitions, thresholds, and the March 2024 switch from FID to INP.
- Largest Contentful Paint (web.dev) What counts as an LCP candidate and how it is reported.
- Optimize Largest Contentful Paint (web.dev) The four LCP parts and target proportions.
- Interaction to Next Paint (web.dev) The three phases and how the reported interaction is chosen.
- Cumulative Layout Shift (web.dev) Impact and distance fractions, session windows, and expected shifts.
- Defining the Core Web Vitals metrics thresholds (web.dev) Why 2.5 s, 0.1, and the 75th percentile.
- Chrome UX Report documentation How the field data behind PageSpeed Insights and Search Console is collected.
- Vodafone: a 31% improvement in LCP increased sales by 8% (web.dev) The A/B test behind the Vodafone numbers in this lesson.
- LCP and INP are now Baseline Newly available (web.dev) Firefox and Safari support for LCP and INP, and why CrUX stays Chrome only.
- web-vitals library (GitHub) The standard way to measure the vitals in your own RUM, with attribution.
Read next
- The critical rendering pathEverything the browser must fetch and compute before the first pixels: HTML, render-blocking CSS, parser-blocking scripts, then style, layout, paint, and composite, and how to count, measure, and shorten it.
- Loading strategiesDeciding what the browser loads first, what it loads later, and what it loads only when needed, so the pixels that matter show up fast.
- Runtime performanceKeeping the page responsive after it loads: finding long tasks on the main thread, splitting or moving that work, and avoiding needless re-renders so every click and keypress paints in under 200 ms.
- Images and mediaSending each user an image of the right size and format, reserving its space, and loading it at the right moment: images are usually the largest share of a page's bytes and its LCP element.
- Web fontsLoading custom fonts without invisible text or a jumping page: choosing
font-display, preloading the right files, subsetting them, and matching the fallback font's size. - Performance budgets and monitoringAgreed limits on what a page may cost (kilobytes of JavaScript, LCP, INP) that CI checks on every pull request and real-user monitoring watches in production, so speed stops getting worse one small change at a time.