Core Web Vitals are Google’s three measures of how a page feels to real visitors: how fast the main content appears (LCP), how quickly the page responds to input (INP), and how stable the layout is while it loads (CLS). Google uses them as part of how it assesses page experience in Search, though good scores alone do not guarantee rankings. They matter more as a plain measure of whether your pages feel fast. This guide covers the thresholds, the usual causes of failure, and the fixes, with code.
A page passes when it meets the “good” threshold for all three at the 75th percentile of real visits. If your site runs on WordPress, Core Web Vitals for scaling WordPress covers the platform-specific work.
The three metrics
Largest Contentful Paint (LCP)
What it measures: when the largest piece of content in the viewport, usually a hero image or headline, finishes rendering.
Good: 2.5 seconds or less.
Common culprits:
- Slow server response
- Render-blocking resources
- Slow resource load times
- Client-side rendering
Interaction to Next Paint (INP)
What it measures: how quickly the page responds visually to clicks, taps and key presses, across the whole visit. It replaced First Input Delay (FID) as a Core Web Vital in March 2024.
Good: 200 milliseconds or less.
Common culprits:
- Heavy JavaScript
- Long tasks blocking the main thread
- Inefficient event handlers
- Third-party scripts
Cumulative Layout Shift (CLS)
What it measures: how much visible content moves unexpectedly while the page loads and is used.
Good: 0.1 or less.
Common culprits:
- Images without dimensions
- Ads and embeds
- Dynamically injected content
- Web fonts causing FOIT/FOUT
Improving LCP
1. Speed up the server response
Time to First Byte (TTFB) is not a Core Web Vital, but every millisecond of it delays LCP. web.dev treats 0.8 seconds or less as a good TTFB.
- Serve pages from a CDN.
- Cache full pages wherever the content allows it.
- Fix slow database queries.
- Move to better hosting if the server itself is the bottleneck.
2. Remove render-blocking resources
CSS:
<!-- Inline the critical CSS needed for the first screen -->
<style>
/* Critical styles here */
</style>
<!-- Load the rest without blocking rendering -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
The onload trick relies on an inline event handler, which a strict Content Security Policy blocks. If you run one, keep the full stylesheet small enough to load normally instead.
JavaScript:
<!-- Defer non-critical JS -->
<script src="app.js" defer></script>
<!-- Or async for independent scripts -->
<script src="analytics.js" async></script>
3. Optimize images
Format:
- AVIF or WebP for photos. Both are usually much smaller than an equivalent JPEG, and both are supported in all current major browsers.
- SVG for icons and logos.
Responsive images let the browser download a size that fits the screen. Use loading="lazy" for images below the fold:
<img
src="image-800.webp"
srcset="image-400.webp 400w,
image-800.webp 800w,
image-1200.webp 1200w"
sizes="(max-width: 600px) 400px,
(max-width: 1200px) 800px,
1200px"
alt="Description"
width="800"
height="600"
loading="lazy"
>
Never lazy-load the image that is likely to be your LCP element. Load it eagerly and give it priority:
<img src="hero-1200.avif" alt="Description" width="1200" height="675" fetchpriority="high">
4. Preload critical resources
Preload resources the browser would otherwise discover late, such as a hero image set in CSS or the font used in the main headline:
<link rel="preload" href="hero-image.avif" as="image" fetchpriority="high">
<link rel="preload" href="critical-font.woff2" as="font" type="font/woff2" crossorigin>
Preload sparingly. Everything you preload competes for bandwidth with everything else.
Improving INP
Most INP problems come from JavaScript keeping the main thread busy when the user interacts. Our INP optimization guide goes deeper; these are the core techniques.
1. Break up long tasks
Any task over 50 milliseconds is a long task, and while it runs the browser cannot respond to input. Split the work and yield to the main thread between chunks. scheduler.yield() is the purpose-built API; fall back to setTimeout where it is not supported:
function yieldToMain() {
if (globalThis.scheduler?.yield) return scheduler.yield();
return new Promise((resolve) => setTimeout(resolve, 0));
}
async function processInChunks(items, processItem, chunkSize = 10) {
for (let i = 0; i < items.length; i += chunkSize) {
items.slice(i, i + chunkSize).forEach(processItem);
await yieldToMain();
}
}
2. Give feedback first, then do the work
Slow: the heavy work runs before the browser can paint any response to the click.
button.addEventListener('click', () => {
processLargeDataset();
updateUI();
});
Faster: update the UI, yield so the browser can paint it, then do the heavy work.
button.addEventListener('click', async () => {
button.classList.add('loading');
await yieldToMain();
processLargeDataset();
updateUI();
button.classList.remove('loading');
});
Wrapping the heavy work in requestAnimationFrame does not help on its own: the callback runs before the next paint, so the work still delays the frame.
3. Ship less JavaScript
- Remove unused code.
- Split code by route.
- Load non-critical features only when they are needed.
- Replace heavy libraries with lighter ones or with platform features.
4. Tame third-party scripts
Third-party scripts (chat widgets, tag managers, embeds) are a common cause of poor INP. Load the ones that are not essential after the page has loaded:
<!-- Load after page is interactive -->
<script>
window.addEventListener('load', () => {
const script = document.createElement('script');
script.src = 'https://third-party.com/widget.js';
document.body.appendChild(script);
});
</script>
Improving CLS
1. Always include dimensions
<!-- Width and height let the browser reserve the space before the image loads -->
<img src="photo.webp" width="800" height="600" alt="Photo">
<!-- Or reserve the space in CSS -->
<style>
.image-container {
aspect-ratio: 16 / 9;
}
</style>
2. Reserve space for ads and embeds
.ad-container {
min-height: 250px; /* the height of the ad slot */
}
3. Do not insert content above existing content
A banner or cookie notice injected at the top after the page renders pushes everything down. Reserve its space in advance, overlay it, or place it below the content the visitor is reading.
4. Stop web fonts from moving the text
When a web font replaces its fallback, text can reflow if the two fonts have different metrics. font-display: optional avoids the swap entirely on slow connections. With swap, tune a fallback font to match the web font’s metrics so the switch barely moves anything:
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap;
}
/* A local fallback adjusted to match CustomFont's size and line metrics */
@font-face {
font-family: 'CustomFont Fallback';
src: local('Arial');
size-adjust: 105%;
ascent-override: 95%;
}
body {
font-family: 'CustomFont', 'CustomFont Fallback', sans-serif;
}
The percentages here are placeholders; calculate them for your own font pairing.
Measuring performance
Lab tools
- Lighthouse, in Chrome DevTools and PageSpeed Insights.
- WebPageTest, for detailed waterfalls and filmstrips.
- The Chrome DevTools Performance panel, for profiling long tasks and interactions.
Lab tools are for diagnosis. They run on one device and one connection, so they cannot tell you what real visitors experience.
Field data
- Chrome User Experience Report (CrUX), the real-user data behind PageSpeed Insights’ field section.
- Google Search Console’s Core Web Vitals report, grouped by URL.
- The
web-vitalsJavaScript library, for your own monitoring.
Field data is what Google assesses, and it is the number to manage.
Setting up monitoring
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
});
navigator.sendBeacon('/analytics', body);
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
Advanced techniques
Resource hints
<!-- Resolve DNS early for third-party domains -->
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<!-- Open the connection early to a critical third party -->
<link rel="preconnect" href="https://cdn.example.com">
<!-- Fetch a likely next page in the background -->
<link rel="prefetch" href="/next-page/">
In Chromium browsers, the Speculation Rules API goes further and can prerender likely next pages so they open almost instantly. Browsers that do not support it ignore it.
Service workers
Cache critical resources so repeat visits load from the device:
// sw.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'/',
'/styles.css',
'/app.js',
'/critical-image.webp'
]);
})
);
});
Change the cache name (v1) whenever these files change, and delete old caches in the activate event, or visitors will keep seeing stale assets.
Edge computing
Running code at CDN locations (Cloudflare Workers, Netlify and Vercel edge functions, AWS Lambda@Edge) moves personalization, redirects and A/B tests closer to the visitor without giving up a cached page.
Set a performance budget
A budget turns “keep it fast” into numbers a build can check. A reasonable starting point for a content site, compressed sizes per page:
| Resource | Budget |
|---|---|
| Total page weight | Under 500 KB |
| JavaScript | Under 100 KB |
| Images | Under 300 KB |
| Fonts | Under 50 KB |
| LCP | 2.5 s or less |
| INP | 200 ms or less |
| CLS | 0.1 or less |
Adjust it to the site, then enforce it in CI so a regression fails the build instead of reaching visitors. Frameworks that ship little JavaScript by default, such as Astro, make these numbers much easier to hold. Our performance engineering work is built around budgets like this one.
Checklist
Quick wins
- Enable Brotli or gzip compression.
- Set long cache headers on versioned assets.
- Compress and resize images.
- Add width and height to every image.
- Defer non-critical JavaScript.
Medium effort
- Lazy-load images below the fold, and never the LCP image.
- Inline critical CSS.
- Serve the site from a CDN.
- Tune web font loading.
- Remove unused CSS and JavaScript.
Advanced
- Add a service worker.
- Monitor field data with the
web-vitalslibrary. - Split code by route.
- Cache at the edge.
- Enforce performance budgets in CI.
Where to start
Open the Core Web Vitals report in Search Console and find the metric and template that fail most. Fix that first. Failing URLs are usually grouped by template, so one fix often covers many pages.