Google’s Core Web Vitals program emerged from a straightforward problem: lab benchmarks and synthetic audits measure what engineers care about, not what users experience. The Chrome team wanted a small set of metrics grounded in real-world field data — measurements collected from actual Chrome sessions across millions of URLs — that could serve as a proxy for perceived quality. The result is three metrics, each targeting a distinct dimension of the loading and interaction experience: Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS).

These metrics feed the Chrome User Experience Report (CrUX), which aggregates field data from opted-in Chrome users. PageSpeed Insights pulls from CrUX when enough data exists for a URL, so the scores you see there reflect real users, not a Lighthouse run in a data center. That distinction matters: a URL can pass every Lighthouse audit and still show poor field data if it serves a slow CDN edge to a specific region, or if its JavaScript blocks the main thread only on lower-end Android devices.

What LCP Actually Measures

Largest Contentful Paint marks the point in the page load when the largest image or text block visible in the viewport has finished rendering. The LCP specification defines eligible elements as <img>, <image> inside SVG, <video> poster frames, elements with a CSS background-image, and block-level text nodes. The browser continuously updates its candidate as more content loads; the final LCP entry is the last candidate before the user first interacts with the page.

The “good” threshold is 2.5 seconds at the 75th percentile of field data. That percentile choice is deliberate: optimizing for p75 forces you to address the experience of users on slower connections and devices, not just the median.

Common LCP Culprits

Unoptimized hero images. A 2MB JPEG hero that ships to every viewport is the single most common LCP killer. The fix involves multiple layers: serving modern formats (WebP, AVIF) via <picture> or server-side content negotiation, generating responsive image variants with srcset, and — critically — adding fetchpriority="high" to the LCP image element so the browser deprioritizes nothing while waiting for it.

<picture>
  <source srcset="/hero.avif" type="image/avif">
  <source srcset="/hero.webp" type="image/webp">
  <img
    src="/hero.jpg"
    alt="Descriptive text"
    width="1200"
    height="630"
    fetchpriority="high"
    loading="eager"
  >
</picture>

Render-blocking resources. Stylesheets in <head> block rendering until they download and parse. A 200KB unminified CSS bundle loaded from a slow third-party CDN can push LCP well past 2.5 seconds even on a fast connection. The diagnostic is the “Eliminate render-blocking resources” entry in Lighthouse and the waterfall view in DevTools Network panel — look for resources that delay the First Contentful Paint (FCP) row. Solutions include inlining critical CSS, deferring non-critical styles, and moving third-party scripts to defer or async.

Slow Time to First Byte (TTFB). LCP cannot be good if TTFB is bad — the browser cannot render content it has not received. A TTFB above 600ms at p75 almost always causes LCP problems. Causes include origin server processing time, no CDN caching for HTML, and geographic distance to the origin. Server-Side Rendering (SSR) with edge caching, or static generation for content that doesn’t need real-time data, are the most reliable fixes.

Client-rendered text. If your hero heading or product name renders via JavaScript after the page loads, the LCP element may not paint until the JS bundle executes and the DOM updates. This is especially common in React, Vue, and Angular applications where the initial HTML is a near-empty shell. Server-side rendering or static generation resolves this — the text is in the HTML, so it’s eligible for LCP from the first paint.

For pages where SSR isn’t practical, a <link rel="preload"> for the LCP image is the fastest single intervention:

<link
  rel="preload"
  as="image"
  href="/hero.avif"
  imagesrcset="/hero-400.avif 400w, /hero-800.avif 800w, /hero.avif 1200w"
  imagesizes="100vw"
>

INP: The Metric That Replaced FID

In March 2024, Interaction to Next Paint replaced First Input Delay as a Core Web Vital. The change reflects a genuine measurement gap: FID captured only the delay before the browser started processing the first interaction, not the actual time to render a response. A page could have a good FID score — say, 50ms — and still feel sluggish because the interaction itself triggered 800ms of JavaScript execution before the screen updated.

INP measures the full interaction latency: from when the user initiates an interaction (click, keypress, tap) to when the browser renders the next frame in response. The INP specification defines the score as the worst interaction on the page, with a small budget of “outlier forgiveness” for pages with many interactions. The good threshold is 200ms; above 500ms is poor.

What Triggers Long INP

Long tasks on the main thread are the root cause. The browser’s main thread handles JavaScript execution, style calculation, layout, and painting — it cannot process a user event while any of those are running. A task longer than 50ms (the Long Tasks threshold) will cause noticeable delay if an interaction arrives during it.

Common sources of long tasks:

  • Event handler JavaScript. A click handler that synchronously re-renders a large list, runs complex calculations, or triggers a cascade of state updates will block the thread. Profile with DevTools Performance panel — record an interaction, then look at the flame chart for long yellow bars below the interaction event.
  • Third-party scripts. Analytics, chat widgets, and A/B testing tools inject code that runs on the main thread. A tag manager loading 12 third-party scripts can generate dozens of long tasks throughout a session.
  • Hydration. In server-rendered JavaScript frameworks, hydration — the process of attaching event listeners to server-rendered HTML — often runs as a single large task. Frameworks like Astro (partial hydration), Qwik (resumability), and React 18’s concurrent features address this with varying degrees of success.

Working Within the JS Execution Budget

The practical discipline for INP is keeping individual tasks under 50ms. For work that cannot be avoided, the tools are:

  • scheduler.postTask() — the Prioritized Task Scheduling API lets you break long work into cooperative chunks with explicit priority. Available in Chromium-based browsers.
  • setTimeout(fn, 0) / queueMicrotask() — blunter instruments, but effective for yielding to the browser between chunks of work.
  • Web Workers — for CPU-intensive work that doesn’t need DOM access (parsing, hashing, sorting large datasets), offloading to a Worker prevents main thread blocking entirely.

The web-vitals JavaScript library provides onINP() — a callback that fires at page unload or visibility change with the final INP value and attribution data identifying which interaction element caused the worst delay. Logging this to your analytics gives you field-level INP data correlated to real user interactions, not synthetic test sessions.

import { onINP } from 'web-vitals/attribution';

onINP(({ value, attribution }) => {
  const { interactionTarget, interactionType, processingDuration } = attribution;
  // Send to your analytics endpoint
  console.log({ value, interactionTarget, interactionType, processingDuration });
});

CLS: Layout Stability and Why It’s Harder Than It Looks

Cumulative Layout Shift measures unexpected movement of visible content during the page lifecycle. The Layout Instability API calculates each shift as impact fraction × distance fraction — roughly, how much of the viewport moved, weighted by how far it moved. CLS is the sum of shift scores in session windows, with the worst session window’s score used as the page’s CLS value. Good is below 0.1; poor is above 0.25.

CLS is deceptive because it often doesn’t manifest in Lighthouse runs — many shifts happen after the page loads, triggered by late-injected content, font swaps, or user interactions. Field data from CrUX frequently shows worse CLS than any lab tool reports.

Common CLS Causes

Images and media without explicit dimensions. When the browser encounters an <img> without width and height attributes, it reserves no space until the image loads. Everything below the image shifts down when the dimensions become known. The fix is always to declare dimensions — CSS aspect-ratio works for responsive images where exact pixel values vary:

img {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
}

Setting width and height attributes on the <img> element is still the more reliable approach, as it works even before the stylesheet loads. See the HTML semantics article for how attribute-based sizing interacts with the document’s rendering pipeline.

Font swap causing text reflow. When a web font loads and replaces the fallback system font, the change in character metrics shifts text blocks — especially if the fallback and web font have different line heights, letter spacing, or character widths. font-display: swap trades invisible text (FOIT) for shifted text (FOUT). font-display: optional is more aggressive — it renders the fallback and abandons the web font if it hasn’t loaded within a small window. For body copy at scale, this often produces better CLS than swap. The web fonts guide covers the tradeoffs in detail.

For swap use cases, the CSS size-adjust, ascent-override, descent-override, and line-gap-override descriptors in @font-face let you tune the fallback font’s metrics to match the web font closely enough that the swap causes minimal reflow:

@font-face {
  font-family: 'FallbackForMyFont';
  src: local('Arial');
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
  size-adjust: 107%;
}

Late-injected banners, cookie notices, and ads. Content inserted above existing page content after load is a reliable CLS source. Cookie consent banners, interstitials, and dynamically sized ad slots all cause shifts if they push content down. The mitigation is space reservation: allocate the element’s space in the initial layout (even if empty), so injected content fills a void rather than displacing existing content.

Animations that trigger layout. CSS properties that affect layout — top, left, width, height, margin, padding — cause layout recalculation and register as layout shifts if they involve visible content. Prefer transform and opacity for animations; they run on the compositor thread and do not contribute to CLS.

Tooling for Diagnosis

PageSpeed Insights — the entry point for most developers. It surfaces both field data (from CrUX) and lab data (from Lighthouse). When field data exists, it’s the authoritative signal. Lab data is useful for debugging because it’s reproducible.

Chrome DevTools Performance panel — record a trace while loading or interacting with the page. The Experience track shows layout shift rectangles; the Main thread track shows long tasks. Click any CLS entry to see which elements shifted and by how much. For INP, record during an interaction and look at the event processing chain.

CrUX Dashboard — Google’s Looker Studio CrUX dashboard template visualizes field data over time by URL or origin. Useful for detecting regressions after deploys.

web-vitals JS library — the canonical way to collect real-user metric data. All three metrics expose attribution details that identify the specific elements, interactions, or shifts responsible for a poor score. Pipe this data to your existing analytics or a purpose-built RUM (Real User Monitoring) platform to track p75 values across your user population over time.

The gap between lab and field data is where most CWV investigations start. A page that scores green in Lighthouse but red in CrUX is usually suffering from a condition the lab can’t reproduce: a slow CDN edge for a specific region, a third-party script that loads inconsistently, or a layout shift triggered by user behavior that Lighthouse’s synthetic session never exercises. Field data tells you there’s a problem; lab tools and DevTools help you find it.