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.
| Metric | Good |
|---|---|
| LCP | 2.5 s or less |
| INP | 200 ms or less |
| CLS | 0.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.
Measures LCP, CLS and INP for this page in your browser, where supported.
| Metric | Measures | Good | Needs improvement | Poor |
|---|---|---|---|---|
| LCP: Largest Contentful Paint | Loading: when the biggest image or text block appears | 2.5 s or less | 2.5 to 4 s | Over 4 s |
| INP: Interaction to Next Paint | Responsiveness: delay from a tap, click or key to the next frame | 200 ms or less | 200 to 500 ms | Over 500 ms |
| CLS: Cumulative Layout Shift | Visual stability: how much content jumps around | 0.1 or less | 0.1 to 0.25 | Over 0.25 |
| Field (real users) | Lab (one test run) | |
|---|---|---|
| Source | Chrome UX Report, your own RUM script | Lighthouse, PageSpeed Insights lab section, DevTools |
| Devices | Your real visitors' phones, networks and locations | One simulated device and network |
| INP | Measured from real interactions | Not measured; Total Blocking Time is the stand-in |
| Used for Google's assessment | Yes, the 28-day field data | No, it is a diagnostic |
| Use it to | Know whether there is a problem | Find and debug the cause |
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">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">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">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>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;
}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>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>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>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=86400A 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();
}
}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));
});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);
});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)));
}} />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);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;
}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"; });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);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>Maps, videos and iframes often load late. aspect-ratio holds their box open.
.video-embed { aspect-ratio: 16 / 9; width: 100%; }Content inserted above what people are reading pushes it down. Give the slot a minimum height in advance.
.promo-slot { min-height: 90px; }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; }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); }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; }A loading placeholder should take the same space as what replaces it.
.card-skeleton { height: 280px; border-radius: 16px; background: #f1f1f1; }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 | Usual cause | First fix |
|---|---|---|
| WordPress | Many plugins adding scripts and CSS on every page | Remove unused plugins; load scripts only where needed |
| Shopify | Apps injecting scripts and late banners | Audit apps; reserve space for app blocks |
| Single page apps (React, Vue) | Large JavaScript bundles, content built on the client | Prerender or server-render; split code by route |
| Website builders | Big uncompressed hero images | Upload sensibly sized images; most builders convert them |
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.
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.
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.
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.
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.
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.