Core Web Vitals are Google's attempt to turn "does this page feel good to use" into three numbers. They are part of the page experience ranking signal, but the better reason to care is simpler: they map almost perfectly onto the things that make users leave.
There are three, and each one measures a different moment in the page lifecycle:
- LCP — how long until the page looks loaded.
- INP — how quickly the page responds when you interact with it.
- CLS — how much the layout moves around while you are reading.
The thresholds
A metric is measured at the 75th percentile of real user visits. Hitting the "good" number on your laptop means nothing; three quarters of your users on real devices have to hit it.
Largest Contentful Paint (LCP)
- Good: ≤ 2.5 s
- Needs improvement: 2.5–4.0 s
- Poor: > 4.0 s
Interaction to Next Paint (INP)
- Good: ≤ 200 ms
- Needs improvement: 200–500 ms
- Poor: > 500 ms
Cumulative Layout Shift (CLS)
- Good: ≤ 0.1
- Needs improvement: 0.1–0.25
- Poor: > 0.25
INP replaced First Input Delay in March 2024. FID only measured the delay before the first interaction started processing — a metric so easy to pass that almost every site did. INP measures the full latency of all interactions, from input to the next painted frame, and it is genuinely harder.
LCP: largest contentful paint
LCP is the render time of the largest image or text block visible in the viewport. Usually a hero image, a heading, or a video poster.
What makes it slow
- Slow server response (TTFB). Everything else waits on this.
- Render-blocking resources. CSS in the
<head>and synchronous scripts block the first paint entirely. - The LCP image is discovered late. If it is set via CSS
background-imageor injected by JavaScript, the preload scanner cannot find it in the HTML and the download starts hundreds of milliseconds late. - The image is enormous. A 3 MB PNG where a 150 KB WebP would do.
- Lazy loading your hero.
loading="lazy"on an above-the-fold image is an own goal — it explicitly delays the thing you are being measured on.
Fixes
<!-- Tell the browser what matters, as early as possible -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
<!-- Or, on the element itself -->
<img src="/hero.webp" fetchpriority="high" width="1200" height="630" alt="..." />
- Serve the LCP element from server-rendered HTML, not from a client-side fetch.
- Preconnect to third-party origins you cannot avoid:
<link rel="preconnect" href="https://cdn.example.com" />. - Defer non-critical JS:
<script src="/analytics.js" defer></script>. - Compress and convert images to WebP/AVIF, and serve a
srcsetso phones do not download desktop-sized files. - Put a CDN in front of static assets.
INP: interaction to next paint
INP measures the worst (roughly) interaction latency across the whole page visit: from the moment the user clicks, taps or types, until the browser paints the resulting frame.
What makes it slow
The main thread is busy. That is essentially the whole story — JavaScript is single-threaded, and while a long task runs, nothing can be painted.
Common culprits:
- Long tasks over 50 ms — big hydration passes, expensive state updates, heavy loops.
- Re-rendering an entire tree on every keystroke of a search input.
- Layout thrashing — reading
offsetHeightand then writing a style in the same loop, forcing synchronous reflow over and over. - Third-party scripts — chat widgets, tag managers and A/B testing tools that hijack the main thread at the worst moment.
Fixes
// Break a long task so the browser can paint between chunks
async function processAll(items) {
for (const item of items) {
process(item);
// yields to the main thread
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
- Debounce expensive handlers on input events.
- Virtualize long lists — render the visible rows, not all 5,000.
- Move genuinely heavy computation into a Web Worker.
- In React, mark non-urgent updates with
useTransition/useDeferredValueso typing stays responsive while the filtered list catches up. - Give feedback immediately. Paint the pressed state or a spinner in the same frame, then do the work.
- Audit your third-party scripts. Load them with
defer, or after the first interaction.
CLS: cumulative layout shift
CLS is the sum of unexpected layout shifts during the page's lifetime. Score = impact fraction × distance fraction. The user-facing version: you go to tap a link, an ad loads above it, and you tap something else.
What makes it bad
- Images without dimensions. The browser reserves zero space, then reflows everything when the image arrives.
- Ads, embeds and iframes with no reserved container.
- Web fonts — text renders in the fallback font, then reflows when the webfont swaps in (FOUT).
- Content injected above existing content — cookie banners, promo bars, "you have 1 new message" toasts pushed into the flow.
- Animating
width,height,toporleft, which trigger layout on every frame.
Fixes
/* Reserve the space before the content exists */
.ad-slot {
min-height: 250px;
}
/* Let the browser compute the box from the ratio */
img,
video {
aspect-ratio: 16 / 9;
width: 100%;
height: auto;
}
- Always set
widthandheighton<img>, even when CSS resizes it. font-display: swapplussize-adjuston the fallback, or self-host withnext/font, which handles the metric matching for you.- Overlay injected UI (banners, toasts) with
position: fixedinstead of pushing the document. - Animate
transformandopacityonly — they run on the compositor and never cause layout shift.
Lab data vs field data
This distinction explains most "but Lighthouse said 100" confusion.
Lab data — a simulated load on a throttled connection, in a controlled environment. Lighthouse, PageSpeed Insights' lab section, DevTools. It is reproducible and great for debugging, but it cannot measure INP properly, because there is no real user clicking things.
Field data — anonymized measurements from real Chrome users, aggregated in the Chrome UX Report (CrUX). PageSpeed Insights' top section, the Core Web Vitals report in Search Console. This is what ranking uses.
A perfect lab score with failing field data usually means your real users are on slower devices and worse networks than your simulation assumed, or that the interactions they perform are heavier than the page load you measured.
Measuring it yourself
The web-vitals library reports all three from real sessions:
import { onCLS, onINP, onLCP } from "web-vitals";
function send(metric) {
navigator.sendBeacon("/analytics", JSON.stringify(metric));
}
onCLS(send);
onINP(send);
onLCP(send);
In Next.js there is a built-in hook — no dependency required:
"use client";
import { useReportWebVitals } from "next/web-vitals";
export function WebVitals() {
useReportWebVitals((metric) => {
console.log(metric.name, metric.value);
});
return null;
}
Other tools worth having open:
- Chrome DevTools → Performance — record an interaction, find the long task, look at the flame chart.
- PageSpeed Insights — lab and field side by side for any public URL.
- Search Console → Core Web Vitals — groups your URLs into "similar pages" so you can fix a template once instead of a page at a time.
Vitals are also only one input into how a page ranks — metadata, structured data, internal linking and content quality all sit alongside them. If you would rather see all of it in one place than stitch four dashboards together, RankBuddy pulls Search Console and analytics data into a single audit, tracks the metrics on a schedule, and tells you which fix is worth doing first. It also tracks whether AI assistants cite your pages, which is quickly becoming its own traffic channel.
Priority order
If you are starting from a bad score, do them in this order — it is roughly effort-to-impact:
- Set image dimensions. Cheapest possible CLS fix.
- Compress and convert your images. Usually the single biggest LCP win.
- Add
fetchpriority="high"to the LCP image, removeloading="lazy"from it. - Defer or delete third-party scripts. Check what each one is actually worth.
- Break up long tasks in your heaviest interactions.
- Server-render your above-the-fold content.
Related: SEO for frontend developers for the markup side, and the Next.js SEO checklist for the framework-specific implementation.