The Browser Contract

Core Web Vitals for Engineers: LCP, INP and CLS

What each metric measures, its “good” limit at the 75th percentile, why INP replaced FID, and the difference between field data and lab data.

Lesson 0918 min read#performance#web-vitals#seo

On this page

  1. The three metrics and their thresholds
  2. LCP in depth
  3. INP in depth
  4. CLS in depth
  5. Field data vs lab data
  6. Related metrics worth knowing
  7. Common mistakes
  8. Interview questions
  9. Key takeaways
  10. What's next

In one line: Core Web Vitals are three numbers that describe the user's experience. They are measured on real users' devices. LCP shows how fast the main content appears. INP shows how fast the page responds when the user does something. CLS shows how much the layout jumps around. A page "passes" when all three are good at the 75th percentile.

Core Web Vitals are a set of three metrics, defined by Google, that measure how real users experience loading, responsiveness and visual stability. A metric is a number that you can measure and compare. A percentile is a position in a sorted list of results: the 75th percentile is the value that 75% of the results are better than. At a senior level, talk about performance with metrics, limits and causes. Do not just say "it feels slow." These three metrics give everyone the same words. They also lead into the debugging guides in the Scenario Debugging series.

The three metrics and their thresholds

MetricMeasuresGoodNeeds improvementPoor
LCP: Largest Contentful Paint (a paint is the moment something is drawn on screen)Loading: when the largest visible content element is drawn≤ 2.5 s2.5 to 4 s> 4 s
INP: Interaction to Next PaintResponse: how long the page takes to show a visible reaction to clicks, taps and key presses≤ 200 ms200 to 500 ms> 500 ms
CLS: Cumulative Layout ShiftVisual stability: content that moves when you do not expect it≤ 0.10.1 to 0.25> 0.25

"At the 75th percentile" means this. Collect the metric from real page loads and sort them from best to worst. Then look at the value that 75% of visits are better than (the 75th percentile). A page passes a metric if that value is "good". Mobile and desktop are checked separately. So your slowest quarter of users counts. An average can hide problems.

Why INP replaced FID

FID (First Input Delay) was the older responsiveness metric. It measured only the wait before the browser started to handle the first interaction. It missed slow handlers, later interactions, and the time to show the result. INP looks at all interactions during a visit. It measures from the user's action until the next frame is painted. It reports roughly the worst one (on pages with many interactions, it ignores the single worst outlier for every 50 interactions). INP officially replaced FID as a Core Web Vital on 12 March 2024.

LCP in depth

What counts as the LCP element: the largest of these: an <img>, an <image> inside an <svg>, a <video> poster, an element with a CSS background-image loaded through url(), or a block of text. Usually it is the hero image (the big picture at the top) or the main heading.

LCP is the sum of four parts. If you find which part is big, you know what to fix:

PartWhat is slowFixes
TTFB (time to first byte: how long until the first byte of the HTML response arrives)The server or networkCDN (a group of servers around the world that serve copies of your files from a place near the user), caching, streaming HTML, faster backend, fewer redirects (Lesson 00)
Resource load delayThe browser found the image late (CSS background, added by JS, lazy-loaded hero)Put the image in the HTML as <img>, preload it (a <link rel="preload"> tag that tells the browser to fetch a file early), add fetchpriority="high" (an attribute that tells the browser this file is more important than others), never lazy-load the LCP image
Resource load durationA big file on a slow connectionModern formats (AVIF/WebP), the right size with srcset/sizes (attributes that give the browser a list of image sizes so it picks one that fits the screen), compression, CDN
Element render delayThe browser had the file but could not paint it (CSS or JS that blocks rendering, or client-side rendering that waits for JS)Inline critical CSS (put the CSS needed for the first screen straight in the HTML), defer scripts (the defer attribute makes a script wait until the HTML is read), render the content on the server (server-side rendering: the server sends ready HTML so the browser does not wait for JavaScript to build the page), reduce main-thread work
HTML
<!-- A good hero: discoverable early, high priority, correctly sized, never lazy --><img  src="/hero-1200.avif"  srcset="/hero-600.avif 600w, /hero-1200.avif 1200w, /hero-2000.avif 2000w"  sizes="100vw"  width="1200" height="630"  fetchpriority="high"  alt="…"/>

In Next.js, next/image is a component that makes images the right size and format and loads them well. For the LCP image, use fetchPriority="high" (with loading="eager"). Next.js 16 deprecated the older priority prop. It now prefers preload, which adds a preload link. Check the image documentation for your version.

Scenario: LCP is poor only on mobile

Common causes:

  • A huge image sent to a small screen (no srcset).
  • Slow mobile networks, which make the resource load duration long.
  • Heavy JavaScript, which delays rendering on weaker CPUs.
  • Client-side rendering, where content appears only after a big bundle runs.
  • A far-away server.

Compare the four parts in field data to see which one is biggest.

INP in depth

The delay of one interaction has three parts:

  1. Input delay: the main thread was busy, so the event handler could not start.
  2. Processing time: your event handlers (and what they start) are running.
  3. Presentation delay: the browser draws the result after the handlers finish (layout, paint).
CauseFix
Long tasks blocking the threadBreak them up, use scheduler.yield() (pauses your code so the browser can handle input), use Web Workers (background threads for heavy work) (Lesson 08)
Heavy handlersDo the least work right away (update the UI) and do the rest later
Large re-renders in ReactMemoise (memoization means saving the result of a calculation so it is not worked out again; React has useMemo for a value, useCallback for a function and React.memo for a component), split components, virtualise lists (draw only the visible rows), use startTransition (marks an update as not urgent) and useDeferredValue (gives you a delayed copy of a value that can lag behind during a busy render)
Large DOM (thousands of nodes)Virtualise, use content-visibility, simplify
Third-party scriptsDefer them, load them after interaction, or remove them
Layout thrashingBatch reads and writes

The rule is: show a reaction fast. Acknowledge the click with some change in the UI. Do the slow work after the next frame.

CLS in depth

A layout shift happens when a visible element moves between frames and the user did not cause it. The layout shift score for one shift is impact fraction × distance fraction. Impact fraction is how much of the viewport was affected. Distance fraction is how far things moved. You multiply the two. CLS reports the worst "session window" of shifts during the life of the page. A session window is a group of shifts that each happen less than 1 second after the one before, with the whole group lasting at most 5 seconds. Shifts within 500 ms after a user input (such as a click that opens a menu) are not counted.

CauseFix
Images, video or iframes without a sizeSet width and height attributes or CSS aspect-ratio, so space is reserved
Ads, embeds (content from another site, such as a video player) or banners that are added lateReserve a slot with a fixed size (min-height)
Content added above existing content (cookie banners, "related" bars)Add it below the fold (the part you must scroll to see), or overlay it with position: fixed or transform
Web fonts that swap in and change the text sizeA web font is a font file that the page downloads. font-display: swap shows a fallback font first and swaps when the file arrives, which can move text. Use a fallback font with a matching size (size-adjust, ascent-override), or font-display: optional (use the web font only if it is ready very fast). Preload key fonts
Animations that change layout propertiesAnimate transform instead
Skeletons (grey placeholder boxes) that do not match the final sizeMake the skeleton the same size as the real content
CSS
/* Reserve space for an image or video before it loads */.card-img { aspect-ratio: 16 / 9; width: 100%; background: #e5e7eb; }/* A slot for a late-loading ad */.ad-slot { min-height: 250px; }

Field data vs lab data

Field data (RUM)Lab data
SourceReal users on real devices and networksA controlled test (Lighthouse, WebPageTest) on a simulated device
ExamplesCrUX (Chrome UX Report: a public dataset of real Chrome user measurements, used by PageSpeed Insights and Search Console), or your own RUM (real user monitoring: measuring real visits with a script) with the web-vitals library (a small Google library that measures the Core Web Vitals)Lighthouse (a tool that loads a page in a controlled setup and gives scores) in DevTools or CI (automatic checks that run on every code change)
Good forWhat users really experience. Google's page-experience signals use itReproducing and debugging: you can repeat it, it shows a waterfall, and it works before release
WeaknessCannot tell you why. Needs traffic. Comes with a delayMay not match real conditions. Cannot measure INP without interaction (Lighthouse uses TBT as a stand-in)

Which one matters for search ranking? Field data (CrUX). Google uses it, but it is only one small signal among many. Which one do you use to fix things? Both. Field data tells you if and where there is a problem. Lab data lets you reproduce the problem and check your fix.

Collecting your own field data

JavaScript
import { onLCP, onINP, onCLS } from "web-vitals";function send(metric: { name: string; value: number; id: string; rating: string }) {  navigator.sendBeacon("/analytics/vitals", JSON.stringify(metric));}onLCP(send);onINP(send);onCLS(send);

navigator.sendBeacon is a browser function that sends a small request in the background. We use it because it still works when the page is being closed. Group the data by route, device type and connection to find the worst cases. Look at the 75th percentile, not the mean.

MetricMeaningNote
TTFBTime to first byteServer and network. It is one part of LCP. Good is 0.8 s or less
FCPFirst Contentful PaintThe first text or image. Good is 1.8 s or less
TBTTotal Blocking TimeA lab stand-in for responsiveness. For every long task between FCP and TTI, it counts the time beyond 50 ms, and adds these amounts up
TTITime to Interactive: when the page loaded and reliably answers inputRemoved from Lighthouse scoring in version 10

Common mistakes

  1. Quoting averages instead of the 75th percentile.
  2. Improving the Lighthouse score while real users (field data) still have a bad experience.
  3. Lazy-loading the LCP image, or using it as a CSS background.
  4. Talking about FID in a new interview. The answer is INP.
  5. Leaving out image sizes, and then wondering why CLS is high.
  6. Fixing only the desktop when mid-range phones decide the 75th percentile.
  7. Adding a heavy third-party tag (chat, analytics, A/B testing) without measuring its INP cost.

Interview questions

Why did INP replace FID?

FID measured only the input delay of the first interaction. It ignored the processing time, the presentation delay and every later interaction. INP measures the full delay (input delay + handlers + next paint) across all interactions in a visit. It reports roughly the worst one. This shows better how responsive the page feels the whole time. It became a Core Web Vital on 12 March 2024.

Lab vs field data: which one matters for search ranking?

Field data matters. Specifically, it is the CrUX data at the 75th percentile for real users. Lab tools such as Lighthouse are for finding and reproducing problems. They cannot measure INP directly. I use field data to decide what to fix. I use lab data to find and check how to fix it.

Scenario: LCP is poor only on mobile. What are the likely causes?

The likely causes are these. Images are too big and have no srcset. Slow mobile networks make the load duration longer. A heavy JS bundle or client-side rendering delays the paint on slow CPUs. CSS blocks rendering. The hero image is found late. The origin server is far away, or the cache is cold. I split LCP into TTFB, load delay, load duration and render delay to see which one is biggest.

How do you prevent layout shift from images or ads that load late?

I reserve space. I set width and height or aspect-ratio on media. I give ad and embed slots a fixed min-height. I add dynamic content below existing content, or as an overlay. I use fallback fonts with a matching size. I animate with transform.

What are the thresholds and how are they evaluated?

LCP ≤ 2.5 s, INP ≤ 200 ms and CLS ≤ 0.1 are "good". They are checked at the 75th percentile of real page loads, separately for mobile and desktop. A page passes the Core Web Vitals check when all three are good.

What are the four parts of LCP and why do they matter?

They are TTFB, resource load delay, resource load duration and element render delay. When you split LCP into these parts, a vague "it is slow" becomes a clear cause. The cause can be the server, how late the image is found, the file size, or something that blocks rendering. Each one has a different fix.

How would you improve INP in a React app?

I find the slow interaction in the Performance panel, or with web-vitals attribution. I break up long tasks and keep handlers light. I reduce how much re-renders (memoisation, keeping state close to where it is used, virtualisation). I mark updates that are not urgent with startTransition or useDeferredValue. I delay or remove heavy third-party scripts.

Why shouldn't you lazy-load the LCP image?

Lazy loading waits to fetch the image until the browser decides it is near the viewport. This makes LCP later. The hero image should be visible in the first HTML and fetched with high priority. In Next.js, use fetchPriority="high" (older versions used the priority prop).

Scenario: CLS is poor only on article pages. What are the likely causes?

Article templates often have ads and embeds that load late, images without sizes, a cookie banner that pushes content down, and web-font swaps. I use the Layout Shift track to see which elements move. I reserve fixed space for ad slots and images (aspect-ratio, min-height). I show banners as overlays and use fallback fonts with a matching size. Then I check field data by template to confirm the fix.

Key takeaways

  • LCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.1 at the 75th percentile. INP replaced FID in March 2024.
  • Split LCP into TTFB, load delay, load duration and render delay. Split INP into input delay, processing and presentation. CLS is almost always about reserved space.
  • Field data shows what users feel. Lab data shows why. You need both.
  • Measure in production with web-vitals. Report percentiles by route and device. Test on slowed-down hardware.

What's next

That completes the Browser Contract series. Next, continue with the Scenario Debugging series, which uses these ideas on real "it's slow" questions. Or read Backend in Depth, which covers the other side of the connection.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Related lessons

Part of the study guide

Related series