Skip to content

Concept in Performance

Core Web Vitals (LCP, INP, and CLS)

Published Updated

Core Web Vitals are three field metrics that Google’s tooling treats as the definition of a good page experience: Largest Contentful Paint for loading, Interaction to Next Paint for responsiveness, and Cumulative Layout Shift for visual stability. A page passes when the 75th percentile of real visits is “good” on all three, and a PWA’s service worker, task scheduling, and layout reservations are the levers that move each one.

Each metric is a PerformanceEntry type exposed through PerformanceObserver, aggregated over a visit, and reported from the field by the web-vitals library or by the Chrome User Experience Report (CrUX). The thresholds come from the Chrome team’s calibration against user-perceived quality (Web Vitals, web.dev).

Metric What it measures Good Needs improvement Poor Entry type
LCP Render time of the largest image or text block in the viewport, relative to navigation start ≤ 2.5 s ≤ 4.0 s > 4.0 s largest-contentful-paint (Chrome 77, Firefox 122, Safari 26.2)
INP The slowest interaction’s latency from input to the next paint, over the whole visit, with a high-percentile cut for pages with many interactions ≤ 200 ms ≤ 500 ms > 500 ms event with interactionId (Chrome 96, Firefox 144, Safari 26.2)
CLS Sum of unexpected layout shift scores in the worst session window ≤ 0.1 ≤ 0.25 > 0.25 layout-shift (Chromium only)

INP replaced First Input Delay as the responsiveness metric in March 2024 (Interaction to Next Paint, web.dev); FID measured only the input delay of the first interaction, whereas INP includes processing time and the paint of the frame after it. Thresholds apply at the 75th percentile of page views, separately for mobile and desktop.

INP and CLS accumulate across a visit, so a single scripted load in Lighthouse or WebPageTest cannot reproduce them; lab tools report Total Blocking Time and a load-time CLS as proxies. CrUX publishes the field distributions for origins with enough traffic, and the web-vitals library gives a page its own numbers through onLCP(), onINP(), and onCLS() callbacks. Use the lab to find causes and the field for the verdict.

Measuring in the field depends on the engine exposing the entry: largest-contentful-paint is in Chrome 77, Firefox 122, and Safari 26.2; event entries with interactionId in Chrome 96, Firefox 144, and Safari 26.2; layout-shift is Chromium-only (BCD api.LargestContentfulPaint, api.PerformanceEventTiming.interactionId, api.LayoutShift). CrUX, and therefore the field numbers search tooling reports, is collected from Chrome only.

  • LCP. A cache-first or stale-while-revalidate strategy in the service worker serves the shell and the hero image from Cache Storage on repeat visits, removing the network from the critical path; see precaching. On first visits the levers are fetchpriority="high" on the LCP image, no lazy loading above the fold, and a server response that includes the image URL in the HTML rather than in a script.
  • INP. Long tasks on the main thread delay the next paint. Split work with scheduler.yield() or setTimeout chunks, move parsing and hydration off the interaction path, and keep event handlers short; see code splitting for shrinking what runs at startup.
  • CLS. Reserve space before content arrives: width and height or aspect-ratio on images and embeds, fixed-height slots for late banners, and font-display: optional or a size-matched fallback so a web font swap does not reflow text; see font and image optimization.

The three entry types are visible in Chrome without any library. The snippet below logs each metric as the browser reports it and degrades to “unsupported” where an engine lacks the entry type, which is the case for layout-shift outside Chromium.

function observe(type, handler) {
if (!('PerformanceObserver' in window)
|| !PerformanceObserver.supportedEntryTypes.includes(type)) {
return false; // entry type not supported here: report nothing for this metric
}
new PerformanceObserver((list) => list.getEntries().forEach(handler)).observe({ type, buffered: true });
return true;
}
observe('largest-contentful-paint', (e) => console.log('LCP candidate', e.startTime, e.element));
observe('layout-shift', (e) => { if (!e.hadRecentInput) console.log('shift', e.value, e.sources); });
observe('event', (e) => { if (e.interactionId) console.log('interaction', e.name, e.duration); });

largest-contentful-paint fires more than once as larger candidates render; the last entry before the first interaction or tab change is the LCP. layout-shift entries carry hadRecentInput, which excludes shifts within 500 ms of a user input, and sources naming the shifted nodes.

Specifications

SpecificationStatus
None.