This Core Web Vitals guide covers the three metrics Google uses to score real-world page experience — Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS) — and, more importantly, how to diagnose and fix each one. I'll focus on concrete changes you can ship in React and Next.js today to pass Core Web Vitals in the field, not just to win a green score in a lab tool. If you get these three right, you improve both the experience and your search rankings.
What are Core Web Vitals?
Core Web Vitals are three user-centric metrics that measure loading, responsiveness, and visual stability. Google evaluates them at the 75th percentile of real page loads, split between mobile and desktop. That means a passing score reflects what most of your users experienced — not just your fastest sessions.
- LCP (Largest Contentful Paint) — loading. Time until the largest visible element (usually a hero image or headline) finishes rendering. Good: ≤ 2.5s. Poor: > 4s.
- INP (Interaction to Next Paint) — responsiveness. How quickly the page visually responds to taps, clicks, and keypresses across the whole visit. Good: ≤ 200ms. Poor: > 500ms.
- CLS (Cumulative Layout Shift) — visual stability. How much content jumps around unexpectedly while the page loads. It's unitless. Good: ≤ 0.1. Poor: > 0.25.
INP replaced First Input Delay (FID) as a Core Web Vital in March 2024. FID only measured the delay before the first interaction started processing. INP is far stricter: it captures the full latency of nearly every interaction on the page and reports the worst one. If your app felt sluggish after the initial load but passed on FID, INP is where that pain now shows up.
Field data vs lab data: measure it the right way
Before you fix anything, know which numbers actually count. There are two kinds of data, and they answer different questions.
- Field data (CrUX) — real Chrome users over a rolling 28-day window. This is what feeds Search Console and what Google ranks on. It's the source of truth.
- Lab data (Lighthouse) — a single synthetic load in a controlled environment. Great for reproducing and debugging, but it does not represent your real users.
A crucial gotcha: Lighthouse cannot measure INP, because there is no real person clicking around. It reports Total Blocking Time (TBT) as a lab proxy instead. To see your true INP and CLS, you need Real User Monitoring. The simplest way is Google's web-vitals library, which reports each metric as it finalizes:
import { onLCP, onINP, onCLS } from 'web-vitals';
function report(metric) {
// metric = { name, value, rating, id, ... }
navigator.sendBeacon('/vitals', JSON.stringify(metric));
}
onLCP(report);
onINP(report);
onCLS(report);
To go from a bad number to a root cause, import from web-vitals/attribution instead. Each metric then carries an attribution object naming the culprit — the exact LCP element, or the DOM target and event type behind your worst interaction — which turns vague scores into an actionable list.
In the Next.js App Router you can skip the wiring and use the built-in useReportWebVitals hook from next/web-vitals, then forward the payload to your analytics endpoint. For a broader look at profiling and monitoring tools, see my rundown of the modern frontend developer's toolkit. For day-to-day checks, PageSpeed Insights (field + lab in one place), the Chrome DevTools Performance panel, and the Search Console Core Web Vitals report cover almost everything.
How to fix Largest Contentful Paint (LCP)
LCP breaks down into four sub-parts: time to first byte (TTFB), resource load delay, resource load time, and element render delay. Open the DevTools Performance panel, find the LCP marker, and see which part dominates — that tells you where to spend effort.
The most common fixes, in rough priority order:
- Don't lazy-load the LCP image. This is the number one mistake. The hero image must load eagerly and with high priority.
- Preload and prioritize the LCP resource with
fetchpriority="high"or a preload link so the browser fetches it early. - Serve modern formats and correct sizes — AVIF or WebP, with responsive
sizesso mobile doesn't download a desktop-sized image. - Cut render-blocking resources — trim critical CSS, and load non-critical fonts and scripts asynchronously.
- Improve TTFB with caching, a CDN, and static or server rendering instead of client-side data fetching for above-the-fold content.
In Next.js, next/image with the priority prop handles most of this for your LCP element automatically — it preloads the image, skips lazy-loading, and sets the aspect ratio:
import Image from 'next/image';
export default function Hero() {
return (
<Image
src="/hero.avif"
alt="Product dashboard"
width={1280}
height={720}
priority // preloads the LCP image, skips lazy-loading
sizes="100vw"
/>
);
}
Rendering strategy matters just as much as images. Static generation and streaming SSR give you a fast TTFB and early paint. I go deeper on this in my Next.js performance guide.
How to fix Interaction to Next Paint (INP)
INP is a main-thread problem. An interaction is slow when JavaScript is busy and can't respond, so a click waits (input delay), then runs heavy work (processing time), then waits again to paint (presentation delay). The fix is almost always the same: stop blocking the main thread with long tasks.
Practical strategies:
- Ship less JavaScript. Smaller bundles mean cheaper hydration and shorter tasks. Moving work to the server with React Server Components is one of the biggest INP wins available — I cover the pattern in Server Components in practice.
- Break up long tasks. Any task over ~50ms is a liability. Split expensive loops and yield to the browser between chunks using
scheduler.yield()(orsetTimeoutas a fallback) so queued interactions can run. - Mark non-urgent state updates as transitions. React's
useTransitionlets you keep typing and clicking responsive while expensive re-renders happen at a lower priority. - Debounce or throttle handlers tied to input, scroll, or resize.
- Animate with CSS (transforms and opacity) instead of JavaScript wherever you can.
Here's the classic useTransition pattern for a search box that filters a large list without janking the input:
import { useState, useTransition } from 'react';
function ProductSearch({ allProducts }) {
const [text, setText] = useState('');
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
function handleChange(e) {
setText(e.target.value); // urgent: keep typing snappy
startTransition(() => setQuery(e.target.value)); // non-urgent: filter later
}
const results = filterProducts(allProducts, query);
// render an input bound to `text`, plus `results`
}
The input updates instantly because that state change is urgent, while the potentially expensive filter runs as an interruptible transition. That difference is exactly what INP rewards.
How to fix Cumulative Layout Shift (CLS)
CLS is usually the easiest to fix once you know the causes. Layout shifts happen when content moves after it has already rendered. The usual suspects:
- Images and videos without dimensions. Always set
widthandheightor a CSSaspect-ratioso the browser reserves space before the file loads.next/imagedoes this for you when you pass width and height. - Ads, embeds, and iframes that inject themselves. Reserve a min-height container up front.
- Web fonts that swap and reflow text (FOUT). Use
next/font, which self-hosts fonts and applies a size-adjusted fallback so the swap doesn't shift layout. - Content inserted above existing content — banners, cookie notices, "loaded" states that push the page down. Reserve their space or overlay them.
- Animating layout properties like
toporheight. Animatetransforminstead; it doesn't trigger layout.
One rule covers most of it: never let something appear without space already reserved for it.
Do Core Web Vitals affect SEO?
Yes. Core Web Vitals are part of Google's page experience signals. They're not the single biggest ranking factor — relevance and content quality still lead — but they act as a real differentiator, especially between pages of similar quality. A slow, unstable page also hurts conversions and bounce rate regardless of ranking, so the work pays off twice. As search itself shifts toward AI answers, technical health remains foundational; I explore that angle in SEO in the age of AI search.
Frequently Asked Questions
What is a good Core Web Vitals score?
To pass, your 75th-percentile field values should be LCP ≤ 2.5s, INP ≤ 200ms, and CLS ≤ 0.1. You need all three in the "good" range on both mobile and desktop. Anything in the middle band is "needs improvement," and the upper band is "poor."
What replaced First Input Delay (FID)?
INP (Interaction to Next Paint) replaced FID as a Core Web Vital in March 2024. FID only measured the delay before the first interaction started; INP measures the full latency of interactions throughout the visit and reports the worst, making it a much more honest measure of responsiveness.
Why do my Lighthouse and PageSpeed field scores disagree?
Lighthouse is lab data from one simulated load on controlled hardware, while the field score is real-user data from CrUX over 28 days. Real users have slower devices, varied networks, and actual interactions, so field numbers are usually worse — and they're the ones Google ranks on. Use lab data to debug, field data to judge.
How long until Core Web Vitals changes show up?
Field data uses a rolling 28-day window, so improvements appear gradually rather than instantly. You can validate a fix immediately with lab tools and your own RUM, but the official Search Console numbers will take a few weeks to fully reflect the change.
Conclusion
Core Web Vitals reward the same things good frontend engineering already values: fast first paint, a responsive main thread, and a stable layout. The practical takeaway is to measure real users first, then target the metric that's actually failing — prioritize and preload your LCP element, ship less JavaScript and yield the main thread for INP, and reserve space for everything to kill CLS. Frameworks like Next.js hand you most of these wins by default through next/image, next/font, and Server Components, so lean on them. Fix the failing metric, verify in the field, and your rankings and your users both benefit. For more, browse the frontend articles on the blog.
