Core Web Vitals: LCP, INP and CLS, with 25 Fixes

Core Web Vitals are three measurements of real visits: Largest Contentful Paint (loading, good at 2.5 seconds or less), Interaction to Next Paint (responsiveness, good at 200 milliseconds or less) and Cumulative Layout Shift (stability, good at 0.1 or less). A page passes when at least 75% of visits meet all three.

Good scores
MetricGood
LCP2.5 s or less
INP200 ms or less
CLS0.1 or less

Updated

The meter below measures this page in your own browser as you use it. Then find the metric you are failing and work through the recipes for it; each one names the cause it fixes and gives the code.

Images are the usual LCP culprit; shrink them with the WebP converter.

Live meter for this page

Measures LCP, CLS and INP for this page in your browser, where supported.

The three metrics

Thresholds (judged on the 75th percentile of real visits)
MetricMeasuresGoodNeeds improvementPoor
LCP: Largest Contentful PaintLoading: when the biggest image or text block appears2.5 s or less2.5 to 4 sOver 4 s
INP: Interaction to Next PaintResponsiveness: delay from a tap, click or key to the next frame200 ms or less200 to 500 msOver 500 ms
CLS: Cumulative Layout ShiftVisual stability: how much content jumps around0.1 or less0.1 to 0.25Over 0.25

Field data vs lab data

Field data vs lab data
Field (real users)Lab (one test run)
SourceChrome UX Report, your own RUM scriptLighthouse, PageSpeed Insights lab section, DevTools
DevicesYour real visitors' phones, networks and locationsOne simulated device and network
INPMeasured from real interactionsNot measured; Total Blocking Time is the stand-in
Used for Google's assessmentYes, the 28-day field dataNo, it is a diagnostic
Use it toKnow whether there is a problemFind and debug the cause

Fixing LCP (recipes 1 to 9)

1. Fetch the hero image first

fetchpriority="high" tells the browser this image matters most. Never lazy-load the LCP image.

<img src="/hero.webp" alt="Freshly baked bread on the counter"
     width="1200" height="800" fetchpriority="high">

2. Preload an LCP image set in CSS

Background images are found late, after the CSS loads. A preload in the head starts the download at once.

<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">

3. Serve the right size

srcset and sizes let a phone download a small file instead of the desktop one.

<img src="/hero-1200.webp"
     srcset="/hero-640.webp 640w, /hero-1200.webp 1200w, /hero-2000.webp 2000w"
     sizes="100vw" alt="..." width="1200" height="800" fetchpriority="high">

4. Use a modern image format

AVIF and WebP are usually much smaller than JPEG at the same quality. picture lets older browsers fall back.

<picture>
  <source srcset="/hero.avif" type="image/avif">
  <source srcset="/hero.webp" type="image/webp">
  <img src="/hero.jpg" alt="..." width="1200" height="800" fetchpriority="high">
</picture>

5. Do not hide text while fonts load

font-display: swap shows fallback text at once, so a heading that is the LCP element is not held back by a font download.

@font-face {
  font-family: "Brand";
  src: url("/fonts/brand.woff2") format("woff2");
  font-display: swap;
}

6. Connect early to the image CDN

preconnect does the DNS, TCP and TLS handshake ahead of time for a third-party origin you will need.

<link rel="preconnect" href="https://images.example-cdn.com" crossorigin>

7. Defer scripts that are not needed to show the page

Scripts in the head without defer or async block rendering. defer keeps order but waits until the HTML is parsed.

<script src="/analytics.js" defer></script>
<script src="/chat-widget.js" async></script>

8. Send real HTML, not an empty shell

If the LCP element is created by JavaScript, nothing can paint until the bundle downloads and runs. Server rendering or prerendering puts it in the first response. See our single page app SEO guide for the options.

<!-- Slow: content arrives after JS -->
<div id="root"></div><script src="/app.js"></script>

<!-- Fast: content is in the HTML -->
<main><h1>Fresh bread daily</h1><img src="/hero.webp" ...></main>

9. Cache the HTML at the edge

A slow server response delays everything. Cache static pages on a CDN and set a sensible Cache-Control.

Cache-Control: public, max-age=0, s-maxage=600, stale-while-revalidate=86400

Fixing INP (recipes 10 to 17)

10. Break up long tasks

A task over 50 ms blocks input. Yield to the main thread between chunks so a click can be handled in between. scheduler.yield is newer; the setTimeout fallback works everywhere.

const yieldToMain = () =>
  globalThis.scheduler?.yield ? scheduler.yield() : new Promise((r) => setTimeout(r, 0));

async function processAll(items) {
  for (const [i, item] of items.entries()) {
    process(item);
    if (i % 50 === 0) await yieldToMain();
  }
}

11. Paint feedback first, work second

Update the button straight away, then do the heavy work after the browser has had a chance to paint.

button.addEventListener("click", () => {
  button.textContent = "Saving...";
  requestAnimationFrame(() => setTimeout(() => saveEverything(), 0));
});

12. Debounce work tied to typing

Filtering a long list on every key press makes typing lag. Wait for a short pause.

let t;
input.addEventListener("input", () => {
  clearTimeout(t);
  t = setTimeout(() => filterList(input.value), 150);
});

13. Mark slow React updates as transitions

startTransition lets React keep the input responsive and render the expensive result when it can.

const [query, setQuery] = useState("");
const [results, setResults] = useState(all);
const [isPending, startTransition] = useTransition();

<input value={query} onChange={(e) => {
  setQuery(e.target.value);
  startTransition(() => setResults(filter(all, e.target.value)));
}} />

14. Delay third-party scripts

Chat widgets and tag managers often run heavy code on load. Load them after the page is idle or on first interaction.

const load = () => {
  const s = document.createElement("script");
  s.src = "https://widget.example.com/loader.js";
  document.body.append(s);
};
("requestIdleCallback" in window) ? requestIdleCallback(load, { timeout: 5000 }) : setTimeout(load, 3000);

15. Keep the DOM small

Huge pages make every style and layout update slower. content-visibility skips rendering for off-screen sections until they are near.

.long-section {
  content-visibility: auto;
  contain-intrinsic-size: auto 800px;
}

16. Do not read layout in a write loop

Reading offsetHeight after each style change forces a layout every time. Read everything first, then write.

// Slow: read, write, read, write...
items.forEach((el) => { el.style.height = el.offsetHeight + 10 + "px"; });

// Fast: all reads, then all writes
const heights = items.map((el) => el.offsetHeight);
items.forEach((el, i) => { el.style.height = heights[i] + 10 + "px"; });

17. Move heavy maths to a Web Worker

Parsing a big file or sorting thousands of rows can run in a worker without blocking clicks.

const worker = new Worker(new URL("./sort.worker.js", import.meta.url), { type: "module" });
worker.postMessage(rows);
worker.onmessage = (e) => render(e.data);

Fixing CLS (recipes 18 to 25)

18. Give images and video a size

width and height let the browser reserve the space before the file arrives. CSS can still make them responsive.

<img src="/team.webp" alt="Our team" width="800" height="533">
<style>img { max-width: 100%; height: auto; }</style>

19. Reserve space for embeds

Maps, videos and iframes often load late. aspect-ratio holds their box open.

.video-embed { aspect-ratio: 16 / 9; width: 100%; }

20. Reserve space for banners and ads

Content inserted above what people are reading pushes it down. Give the slot a minimum height in advance.

.promo-slot { min-height: 90px; }

21. Match the fallback font's size

When the web font swaps in, text can re-wrap. size-adjust on a local fallback makes the two closer in width.

@font-face {
  font-family: "Brand Fallback";
  src: local("Arial");
  size-adjust: 104%;
}
body { font-family: "Brand", "Brand Fallback", sans-serif; }

22. Animate with transform, not top or height

Moving things with transform does not move other elements, so it does not count as a layout shift.

/* Shifts layout */
.toast { transition: top .3s; }
/* Does not */
.toast { transition: transform .3s; }
.toast.show { transform: translateY(0); }

23. Overlay cookie bars instead of pushing content

A bar fixed to the bottom of the screen does not move the page; one inserted at the top does.

.cookie-bar { position: fixed; inset: auto 0 0 0; }

24. Use skeletons the same size as the content

A loading placeholder should take the same space as what replaces it.

.card-skeleton { height: 280px; border-radius: 16px; background: #f1f1f1; }

25. Keep the back/forward cache working

Pages restored from the back/forward cache appear instantly with no new shifts. An unload handler prevents that; use pagehide instead.

// Blocks the back/forward cache in many browsers
window.addEventListener("unload", save);
// Works with it
window.addEventListener("pagehide", save);

Platform notes

Platform notes
PlatformUsual causeFirst fix
WordPressMany plugins adding scripts and CSS on every pageRemove unused plugins; load scripts only where needed
ShopifyApps injecting scripts and late bannersAudit apps; reserve space for app blocks
Single page apps (React, Vue)Large JavaScript bundles, content built on the clientPrerender or server-render; split code by route
Website buildersBig uncompressed hero imagesUpload sensibly sized images; most builders convert them

How this differs from a speed score

A PageSpeed or Lighthouse score blends several lab metrics into one number from a single test. Core Web Vitals are pass or fail measurements of real visits. Use the score to debug, and the field vitals to decide whether there is a problem at all. For a broader speed checklist, see our guide to optimising website speed.

How it works

  1. Check your field data in Search Console's Core Web Vitals report or PageSpeed Insights.
  2. Pick the worst metric and the page group it affects.
  3. Reproduce it in a lab tool (DevTools Performance panel or Lighthouse) to find the cause.
  4. Apply the matching recipe, deploy, and wait for 28 days of field data to update.

Tips

Frequently asked questions

What replaced First Input Delay?

Interaction to Next Paint replaced First Input Delay as a Core Web Vital in March 2024. INP looks at all interactions on a page, not only the first, and includes the time to paint the result.

Why is my Lighthouse score good but Core Web Vitals failing?

Lighthouse is one test on one simulated device. Core Web Vitals use real visitors over 28 days, on slower phones and networks, and include INP, which Lighthouse cannot measure.

Do Core Web Vitals affect Google rankings?

They are part of Google's page experience signals. Google describes relevance and content quality as far more important, so good vitals help most when pages are otherwise similar.

How long until fixes show up?

Field data covers the previous 28 days, so improvements appear gradually over about four weeks after you deploy.

Related: How to optimise website speed, Single page app SEO, HTML images, WebP converter, All free tools.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap