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.
How it works
Section titled “How it works”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.
Field and lab data
Section titled “Field and lab data”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.
Support for the entry types
Section titled “Support for the entry types”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.
Levers a PWA controls
Section titled “Levers a PWA controls”- 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()orsetTimeoutchunks, 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:
widthandheightoraspect-ratioon images and embeds, fixed-height slots for late banners, andfont-display: optionalor a size-matched fallback so a web font swap does not reflow text; see font and image optimization.
Observed behaviour
Section titled “Observed behaviour”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.
See also
Section titled “See also”- Web Vitals (web.dev)
- Interaction to Next Paint (INP) (web.dev)
- Largest Contentful Paint API (w3.org)
- Precaching strategies
- Code splitting with dynamic import()
- Font and image optimization
- Lighthouse and the removed PWA category
Specifications
| Specification | Status |
|---|---|
| None. | |