Blog

Blog · Performance & Optimization

How to Improve Core Web Vitals in 2026: LCP, INP & CLS

A practical guide to the three Core Web Vitals — what LCP, INP and CLS measure, the exact 2026 thresholds, and how to fix each one — with the reasoning and code for every technique.

XCODX Team · 7 min read

Core Web Vitals are Google’s three field metrics for real-world user experience — and a confirmed ranking signal. They are Largest Contentful Paint (LCP) for loading, Interaction to Next Paint (INP) for responsiveness, and Cumulative Layout Shift (CLS) for visual stability. This guide covers the exact 2026 thresholds and concrete fixes for each, with the reasoning and a code example.

1. Know the exact thresholds and how they're scored

Each metric has three bands. LCP: good ≤ 2.5s, needs improvement ≤ 4s, poor > 4s. INP: good ≤ 200ms, needs improvement ≤ 500ms, poor > 500ms. CLS: good ≤ 0.1, needs improvement ≤ 0.25, poor > 0.25. Scores are the 75th percentile of real visits (so you’re optimizing the experience most users get, not the average), and all three must be "good" to pass.

// targets to hit at the 75th percentile of real users
LCP  ≤ 2.5s   (loading)
INP  ≤ 200ms  (responsiveness — replaced FID in 2024)
CLS  ≤ 0.1    (visual stability)

2. Measure field data, not just lab scores

Lighthouse gives a lab estimate on one simulated device; Core Web Vitals are scored on *real users* (field data) via the Chrome UX Report. They often disagree — INP in particular can only be measured from real interactions. Track field data with the web-vitals library sending to your analytics, and read PageSpeed Insights’ field section and Search Console’s Core Web Vitals report.

import { onLCP, onINP, onCLS } from "web-vitals";
function send(m) { navigator.sendBeacon("/vitals", JSON.stringify(m)); }
onLCP(send); onINP(send); onCLS(send);   // real-user field data

3. Optimize the LCP element

LCP is the render time of the largest element in the viewport — usually a hero image or a big heading. Identify it (DevTools marks it), then make it load fast: serve a modern format at the right size, don’t lazy-load it, and raise its priority with fetchpriority="high". Google measured LCP dropping from 2.6s to 1.9s from fetch priority alone.

<img src="hero.avif" fetchpriority="high"
     width="1200" height="675" alt="…">
<!-- never add loading="lazy" to the LCP image -->

4. Cut render-blocking CSS and JavaScript

LCP can’t happen until the browser has processed render-blocking resources in the critical path. Inline the critical CSS and defer the rest, and load scripts with defer/async so parsing isn’t blocked. Every render-blocking request you remove from the critical path pulls LCP earlier.

<style>/* critical above-the-fold CSS inlined */</style>
<link rel="preload" href="/main.css" as="style" onload="this.rel='stylesheet'">
<script src="/app.js" defer></script>

5. Improve Time to First Byte

LCP can’t beat the time it takes to get the first byte of HTML. A slow server, no caching, or a far-away origin inflates TTFB and caps every downstream metric. Serve from a CDN/edge close to users, cache HTML where you can, and use preconnect to warm connections to critical third-party origins.

<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<link rel="dns-prefetch" href="https://cdn.example.com">
<!-- and serve HTML from an edge/CDN with caching -->

6. Fix INP by breaking up long tasks

INP measures the latency from a user interaction to the next paint. The main cause of a poor INP is long JavaScript tasks blocking the main thread when the user clicks or types. Break work into chunks that yield to the browser (scheduler.yield(), feature-detected for Safari), and defer non-urgent work so the response paints quickly.

button.onclick = async () => {
  updateUINow();                         // paint the response first
  if (scheduler?.yield) await scheduler.yield();
  doHeavyFollowUpWork();                 // then the expensive part
};

7. Minimize main-thread JavaScript

Less JavaScript means less parsing, compiling and executing on the main thread — which improves both LCP (the thread is free to render) and INP (it’s free to respond). Code-split, tree-shake, remove unused dependencies, and move heavy computation to a Web Worker. Shipping less JS is the most reliable lever on both metrics.

# find and cut main-thread weight
npx source-map-explorer dist/assets/*.js
npx knip     # unused files/deps/exports

8. Reserve space for media to fix CLS

CLS measures unexpected layout shifts. The most common cause is images, videos and ads without reserved space — content jumps down when they load. Always set width/height (or CSS aspect-ratio) so the browser reserves the box up front. This single habit eliminates most CLS.

<img src="photo.jpg" width="1600" height="900" alt="…"
     style="width:100%;height:auto">

/* or with CSS */
img { aspect-ratio: 16 / 9; width: 100%; height: auto; }

9. Never inject content above existing content

Banners, cookie notices, ads and "you might also like" strips inserted above content already on screen push everything down — a jarring shift and a CLS penalty. Reserve space for them ahead of time, insert them below the fold, or overlay them (fixed/absolute) so they don’t reflow the page.

/* reserve the slot so filling it shifts nothing */
.ad-slot { min-height: 250px; }
/* or take the banner out of flow */
.cookie-bar { position: fixed; bottom: 0; }

10. Preload fonts and use font-display: swap

Web fonts cause both invisible text (hurting LCP if the LCP element is text) and a shift when the fallback swaps to the web font (hurting CLS). Preload the one critical font, use font-display: swap to show fallback text immediately, and size-match the fallback (size-adjust, ascent-override) so the swap barely moves anything.

<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>

@font-face {
  font-family: "Inter"; src: url("/fonts/inter.woff2") format("woff2");
  font-display: swap; size-adjust: 100%;
}

11. Keep interactions off the critical path with transitions

When an interaction triggers an expensive re-render (filtering a big list, switching views), mark it as non-urgent so the browser paints the immediate feedback first. In React, useTransition/useDeferredValue do this; in vanilla JS, update the visible state synchronously and schedule the heavy work after a yield.

// React: keep the click responsive, defer the heavy update
const [isPending, startTransition] = useTransition();
onClick={() => { setActive(id); startTransition(() => setHeavyView(id)); }}

12. Prioritize critical resources

Help the browser fetch the right things first. fetchpriority="high" on the LCP image, preload for a late-discovered critical resource, and preconnect for critical third-party origins all shorten the path to LCP. Use them surgically — over-preloading steals bandwidth from the very resource you’re trying to speed up.

<link rel="preload" as="image" href="hero.avif" fetchpriority="high">
<link rel="preconnect" href="https://api.example.com" crossorigin>

13. Monitor continuously in CI and in the field

Core Web Vitals regress with everyday changes — a new hero image, an added script, a font swap. Catch it two ways: a Lighthouse CI check on pull requests for lab regressions, and real-user monitoring (the web-vitals library) for field truth. Watch the 75th percentile, since that’s what Google scores.

# Lighthouse CI on every PR (lab guardrail)
npx @lhci/cli autorun
# plus web-vitals RUM for the 75th-percentile field data

Core Web Vitals checklist

  • Targets (75th percentile): LCP ≤ 2.5s, INP ≤ 200ms, CLS ≤ 0.1 — pass all three.
  • Measure real-user field data (web-vitals, CrUX), not just Lighthouse.
  • LCP: optimize the LCP element, cut render-blocking resources, improve TTFB.
  • INP: break up long tasks, minimize main-thread JS, defer non-urgent work.
  • CLS: set image/media dimensions, don’t inject content above the fold, tame font swap.
  • Prioritize critical resources with fetchpriority/preload/preconnect.
  • Monitor continuously — Lighthouse CI plus real-user monitoring.

Frequently asked questions

What are the Core Web Vitals thresholds in 2026?
LCP ≤ 2.5s (good), INP ≤ 200ms (good), and CLS ≤ 0.1 (good), measured at the 75th percentile of real users over a rolling 28-day window. A page passes only if all three are in the "good" band. INP replaced FID as a Core Web Vital in March 2024.
Why do my Lighthouse and real-user scores differ?
Lighthouse is a lab test on one simulated device and network; Core Web Vitals are scored from real users (field data) via the Chrome UX Report. Real users have varied devices, networks and interaction patterns, and INP can only be measured from actual interactions. Optimize for field data — that’s what ranks — using the web-vitals library and PageSpeed Insights’ field section.
How do I improve INP specifically?
INP is about main-thread responsiveness. Break up long JavaScript tasks (yield with scheduler.yield(), feature-detected for Safari), minimize main-thread JS, paint immediate feedback before doing heavy follow-up work, and defer non-urgent updates (React useTransition). Then validate with real-user INP data.
Do Core Web Vitals affect SEO?
Yes. Core Web Vitals are part of Google’s page experience signals and a confirmed (if modest) ranking factor. They matter most as a tiebreaker between pages of similar relevance — and beyond ranking, better vitals improve engagement and conversion, which is reason enough to fix them.

Prototype fixes and preview them instantly in XCODX Studio. Dig into each metric with our Image Optimization Guide (LCP/CLS), How to Speed Up JavaScript (INP) and the Modern Web Performance Checklist.