Core Web Vitals: The Developer's Complete Guide
What LCP, INP and CLS actually measure, why your Lighthouse score disagrees with Search Console, and the diagnostic workflow we use to find the one change that fixes each metric.
Core Web Vitals: The Developer's Complete Guide
Core Web Vitals are three user-centric metrics that Google folds into its page experience signals and, more importantly, that correlate strongly with conversion. This guide is for the engineer who has been handed a failing Search Console report and needs to know what to change.
The three metrics in one table
| Metric | Measures | Good | Needs improvement | Poor |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | Loading: when the biggest visible element renders | ≤ 2.5 s | 2.5 to 4 s | > 4 s |
| INP (Interaction to Next Paint) | Responsiveness: delay from any interaction to the next frame | ≤ 200 ms | 200 to 500 ms | > 500 ms |
| CLS (Cumulative Layout Shift) | Visual stability: how much content moves unexpectedly | ≤ 0.1 | 0.1 to 0.25 | > 0.25 |
All three are assessed at the 75th percentile of real user sessions over a 28-day window, segmented by device. A page passes when all three are "Good" on the same device type.
Field data versus lab data
This is the source of most confusion.
Field data comes from the Chrome User Experience Report (CrUX): anonymized metrics from real Chrome users who opted in. This is what Search Console shows and what Google uses. It reflects real devices, real networks and real behaviour (including users who scroll before LCP finishes).
Lab data comes from Lighthouse or WebPageTest: a single synthetic run on a simulated device and network. It is reproducible and great for debugging, but it is not what ranks you.
Where to get field data:
- Search Console, Core Web Vitals report: grouped by URL pattern, shows which templates fail.
- PageSpeed Insights: field data for one URL (and its origin) at the top, lab data below.
- CrUX API or BigQuery: historical and origin-level data for trending.
- Your own RUM: the
web-vitalslibrary sending to your analytics gives you per-session attribution.
import { onLCP, onINP, onCLS } from "web-vitals/attribution"
function send(metric) {
navigator.sendBeacon("/vitals", JSON.stringify(metric))
}
onLCP(send)
onINP(send)
onCLS(send)
The attribution build tells you which element was the LCP, which interaction was slow and which nodes shifted. That is the difference between knowing you have a problem and knowing what to fix.
LCP: Largest Contentful Paint
What counts
The largest image, poster image, background image via url(), or block-level text element visible in the viewport. Candidates are re-evaluated as the page loads; the final LCP is the last candidate before first interaction.
The four sub-parts
Every LCP breaks down into four sequential phases. Measure them before changing anything.
- Time to First Byte: until the first byte of HTML arrives.
- Resource load delay: from TTFB until the browser starts fetching the LCP resource. Caused by late discovery (image injected by JS, CSS background, lazy-loading) or by render-blocking resources ahead of it.
- Resource load duration: how long the resource itself takes. Caused by size, format, missing CDN or slow origin.
- Element render delay: from resource loaded until painted. Caused by render-blocking CSS or JS, or the element being hidden until hydration.
A healthy page spends most of its LCP in TTFB and resource load duration, and almost nothing in the two delay phases. If load delay is above 10% or render delay is above 20%, you have a discovery or blocking problem, not a size problem.
The usual fixes
- Make the LCP image discoverable in HTML with
fetchpriority="high"; never lazy-load it. - Preload it when it is a CSS background:
<link rel="preload" as="image" href="...">. - Serve AVIF or WebP at the rendered size via
srcset. - Inline critical CSS, defer the rest.
- Reduce TTFB with edge caching of HTML.
Full playbook: How to Improve LCP Below 2.5 Seconds.
INP: Interaction to Next Paint
INP replaced First Input Delay in March 2024. Where FID only measured the delay before the first event handler ran, INP measures the full duration from a click, tap or key press to the next frame being painted, for every interaction, and reports a high percentile.
The three phases of an interaction
- Input delay: the main thread is busy (long task, other handlers) so your handler cannot start.
- Processing time: your event handlers run.
- Presentation delay: layout, style and paint before the next frame.
Typical causes and fixes
| Cause | Fix |
|---|---|
| Long tasks from third-party scripts during load | Defer, load on interaction, or move to a web worker |
| Heavy React re-renders on input | Memoize, split state, use transitions for non-urgent updates |
| Synchronous layout reads after DOM writes | Batch reads then writes; avoid offsetHeight in loops |
| Large DOM (over 1,500 nodes) | Virtualize lists, simplify templates |
| Heavy work in the handler itself | Yield to the main thread with scheduler.yield() or setTimeout(0) chunks |
A practical rule: no single task on the main thread should exceed 50 milliseconds. Chrome DevTools Performance panel flags them with a red triangle.
// Yield between chunks so the browser can paint the response to the click
async function processRows(rows) {
for (const chunk of chunksOf(rows, 200)) {
render(chunk)
await scheduler.yield()
}
}
CLS: Cumulative Layout Shift
CLS is the sum of layout shift scores in the worst 5-second window of a session. A shift score is the fraction of the viewport that moved multiplied by the distance it moved. Shifts within 500 milliseconds of a user interaction are excluded.
Common culprits
- Images and iframes without
widthandheightattributes (or CSSaspect-ratio). - Ads, embeds and banners injected above existing content.
- Web fonts swapping in with different metrics.
- Content inserted by late-loading JavaScript (cookie banners, personalization).
- Animations that use
top/leftinstead oftransform.
The fixes
- Always reserve space: dimensions on media, min-height on ad slots, skeletons for async content.
- Preload fonts and use
font-display: optionalorsize-adjustto match fallback metrics. - Insert dynamic content below the fold or in response to a click.
- Animate with
transformandopacityonly.
Deep dive: Understanding Cumulative Layout Shift.
A diagnostic workflow that scales
- Start in Search Console. Which URL groups fail, on which device, on which metric?
- Pick one representative URL per failing group. Run PageSpeed Insights and compare field to lab.
- Collect attribution. Use the
web-vitals/attributionlibrary or DevTools to identify the LCP element, the slow interaction or the shifting nodes. - Fix the template, not the page. One change to the product template fixes 2,000 product pages.
- Verify in the lab, then wait. CrUX needs 28 days; watch the trend in Search Console rather than waiting for the status to flip.
Prioritizing when everything is red
If LCP, INP and CLS all fail, fix in this order:
- CLS is usually cheapest: dimensions on images and reserved ad slots.
- LCP has the biggest conversion impact and often one dominant cause.
- INP tends to require the most engineering, because it is about JavaScript architecture.
Summary
- Core Web Vitals are field metrics at the 75th percentile over 28 days. Lab scores are for debugging.
- LCP is about discovering and delivering one element fast. INP is about keeping the main thread free. CLS is about reserving space.
- Attribution data turns "the metric is bad" into "this element, this script, this shift".
- Fix templates, ship, then watch the trend.
Get Your SEO Audit
Every issue in this article, checked on your site by an auditor and delivered as a prioritized roadmap within 48-72 hours.
See pricing