Core Web Vitals in 2026: How to Actually Fix LCP, INP and CLS
A practical, step-by-step guide to diagnosing and fixing the three Core Web Vitals — with the real-world causes we see over and over again.
Marcus Webb
Senior Writer
Hugo Nina
Fact Checked
If you build websites for a living, three little acronyms decide a surprising amount of your fate: LCP, INP and CLS. Together they form Google’s Core Web Vitals — the user-experience metrics that feed into search ranking, and, more importantly, the numbers that describe whether your site feels fast or broken to a real person on a mid-range phone.
The problem is that most Core Web Vitals advice is either too vague (“optimize your images!”) or too exotic (edge-rendered streaming islands, anyone?). After years of fixing these metrics on production sites — e-commerce, content sites, dashboards — I can tell you the truth: almost every failing score comes from a short list of common causes. This guide walks through them, metric by metric.
What the three metrics actually measure
Before fixing anything, be sure you know what you’re looking at:
| Metric | Measures | Good | Needs work | Poor |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | Time until the biggest visible element renders | ≤ 2.5s | 2.5–4.0s | > 4.0s |
| INP (Interaction to Next Paint) | Worst-case delay between a user interaction and the next visual update | ≤ 200ms | 200–500ms | > 500ms |
| CLS (Cumulative Layout Shift) | How much visible content jumps around while loading | ≤ 0.1 | 0.1–0.25 | > 0.25 |
Two details people constantly miss:
- Google evaluates the 75th percentile of real users (via the Chrome UX Report, or CrUX). Your fibre-connected MacBook means nothing; the metric is about your slowest quarter of visitors.
- Lab tools like Lighthouse simulate a device. They’re great for debugging, but they can pass a page that fails in the field and vice versa. Always start from field data.
Fixing LCP: find the element, then follow the chain
LCP is the most mechanical of the three. Something is your largest element — usually a hero image, a heading or a video poster. Your entire job is to make that one thing appear faster.
Step 1: identify the LCP element
Open Chrome DevTools → Performance panel → record a page load → look for the “LCP” marker, which names the exact element. Or run this in the console:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('LCP element:', entry.element, entry.startTime);
}
}).observe({ type: 'largest-contentful-paint', buffered: true });
Step 2: attack the usual suspects in order
If it’s an image (it usually is):
- Serve it in AVIF or WebP, sized close to its rendered dimensions.
- Add
fetchpriority="high"and make sure it is not lazy-loaded.loading="lazy"on the LCP image is the single most common self-inflicted LCP wound on the web. - Preload it if it’s referenced from CSS:
<link rel="preload" as="image" href="/hero.avif">. - Serve it from the same origin or an already-connected CDN — a new TLS connection can cost 300–600ms on mobile.
If it’s text, your bottleneck is almost always render-blocking resources:
- Inline critical CSS or split your stylesheet so above-the-fold rules arrive first.
- Check for synchronous third-party scripts in the
<head>. Every tag manager, A/B testing snippet and chat widget wants to be there; they can all wait. - Use
font-display: swapso text renders in a fallback font instead of waiting.
Step 3: fix the server if you must
If TTFB (time to first byte) is over ~800ms, no amount of frontend work saves you. Static generation or good caching is the cheat code here — it’s a big part of why we built this site with a static-first framework, as we explain in our Astro blog tutorial.
Fixing INP: the JavaScript tax collector
INP replaced First Input Delay in March 2024, and it is much stricter. FID only measured the delay before your handler started; INP measures the whole story — input delay, processing time, and the time until the browser actually paints the response. Every interaction during the entire page lifetime counts, and your worst one (roughly) becomes your score.
In practice, INP failures come from long tasks: JavaScript that blocks the main thread for more than 50ms. Here’s the debugging loop that works:
- Record a trace in DevTools Performance while interacting with the page (click menus, type in inputs, open modals).
- Look for red-flagged long tasks attached to your interactions.
- Attribute them. Common culprits, in the order I usually find them:
- Huge hydration work in JavaScript frameworks (see our breakdown of server vs client components for why this happens).
- Analytics and tag-manager scripts listening to every click.
- State updates that re-render far more UI than necessary.
- Synchronous
localStorageor heavy JSON parsing inside handlers.
The three fixes that pay for everything
Yield to the main thread. Break long work into chunks so the browser can paint between them:
async function processChunks(items, chunkSize = 50) {
for (let i = 0; i < items.length; i += chunkSize) {
items.slice(i, i + chunkSize).forEach(processItem);
// Let the browser breathe (Chrome 115+):
if ('scheduler' in window && 'yield' in scheduler) {
await scheduler.yield();
} else {
await new Promise((r) => setTimeout(r, 0));
}
}
}
Render the visual response first, do the work second. If a click opens a modal and fetches data, show the modal skeleton immediately and fetch afterwards.
Ship less JavaScript. Boring but undefeated. Every framework byte is parsed, compiled and executed on that same main thread your users are trying to interact with.
Fixing CLS: stop moving the furniture
CLS is the pettiest metric — and the most fixable. Layout shift happens when content appears or resizes after initial render and pushes other content around. The causes are boringly consistent:
Images without dimensions
Always set width and height (or aspect-ratio in CSS). The browser then reserves the space before the image loads:
<img src="/chart.webp" alt="Sales chart" width="800" height="450" />
Injected content: ads, banners, embeds
Anything that arrives late — cookie banners, ad units, YouTube embeds — must have its space reserved in advance with a fixed min-height container. This matters doubly if you plan to run display advertising: an ad unit that expands the page on load will wreck both your CLS and your users’ patience. (It’s exactly why the ad containers on this site are fixed-height placeholders.)
Web fonts swapping
A fallback font and a web font with very different metrics cause visible reflow when the swap happens. Use font-display: swap plus the size-adjust / ascent-override descriptors, or use a tool that generates adjusted fallbacks automatically. Better yet, use one of the excellent system font stacks for body text and save the custom font for headings.
Animations that use layout properties
Never animate top, left, width or height for entrance effects. Animate transform and opacity — they run on the compositor and don’t shift anything.
Measure like you mean it
The virtuous loop for Core Web Vitals work looks like this:
- Field data first. Check the CrUX dashboard or PageSpeed Insights’ “Discover what your real users are experiencing” section. This tells you whether you have a problem and on which metric.
- Lab reproduction second. Use Lighthouse and the Performance panel with CPU/network throttling to reproduce and debug.
- Instrument with the
web-vitalslibrary so you see your own numbers per page and per device class:
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
- Fix one thing, deploy, wait for field data to move. CrUX aggregates over 28 days, so don’t panic-refresh; your own instrumentation will confirm direction sooner.
What actually moves rankings (and what doesn’t)
Let’s be honest about the SEO angle, because that’s why many teams fund this work. Core Web Vitals are a ranking signal, not a ranking hammer. Great content with a “needs improvement” score will still outrank thin content with perfect vitals. What the vitals do reliably affect:
- Bounce and conversion. The correlation between LCP and bounce rate is brutal and well-documented across industry studies.
- Tie-breaking in competitive niches. When ten pages answer the same query equally well, speed helps.
- Crawl efficiency and user trust — a stable, fast page simply gets consumed more.
If your framework choice is fighting you on all three metrics, it may be time to reconsider it — our State of JavaScript Frameworks in 2026 looks at which stacks make performance easy versus possible-but-painful. And where you host matters too: our hosting comparison covers the CDN and caching behavior of the big three free tiers.
Final Thoughts
Core Web Vitals work is unglamorous, but it’s some of the highest-leverage engineering you can do on the web: find the LCP element and make it arrive sooner; hunt down long tasks and break them up; reserve space for everything that loads late. Do those three things and you’ll pass — no heroics, no rewrite, no exotic architecture required.
The meta-lesson is worth internalizing: performance is not a project, it’s a budget. Sites don’t get slow in one bad deploy; they get slow one “tiny” script at a time. Put a performance check in CI, watch your field data monthly, and you’ll never need a guide like this again.