Core Web Vitals Explained: LCP, INP, and CLS — Diagnose and Fix
What are Core Web Vitals?
Core Web Vitals are three specific metrics that Google uses to measure real-world user experience on your website. They became a Google ranking signal in 2021 and remain central to the Page Experience system.
The three metrics are:
- LCP (Largest Contentful Paint) — How fast the main content loads. Measures loading performance.
- INP (Interaction to Next Paint) — How fast the page responds to user input. Replaced FID in March 2024. Measures interactivity.
- CLS (Cumulative Layout Shift) — How much the page layout shifts unexpectedly. Measures visual stability.
Each metric has three thresholds:
- Good (green) — LCP < 2.5s, INP < 200ms, CLS < 0.1
- Needs Improvement (yellow) — LCP 2.5-4s, INP 200-500ms, CLS 0.1-0.25
- Poor (red) — LCP > 4s, INP > 500ms, CLS > 0.25
Google evaluates these at the 75th percentile of all page loads — meaning 75% of your visitors must have a "Good" experience for the metric to pass.
Tip
Check your Core Web Vitals in Google Search Console under "Experience > Core Web Vitals." This shows real-user data (CrUX) grouped by mobile and desktop, with specific URLs that need improvement.
LCP: Largest Contentful Paint
LCP measures when the largest visible element in the viewport finishes rendering. This is usually the hero image, a large heading, or a video poster. It's the best proxy for "when does the page look loaded?"
Common LCP elements:
<img>elements (most common — hero images, product images)<video>poster images- Elements with CSS
background-image - Large text blocks (
<h1>,<p>)
What causes bad LCP:
- Slow server response (high TTFB)
- Render-blocking CSS and JavaScript
- Large, unoptimized LCP image
- Client-side rendering (content not available until JS executes)
- Web font loading delays (FOIT — flash of invisible text)
How to fix LCP:
<!-- 1. Preload the LCP image -->
<link rel="preload" as="image" href="/hero.avif" type="image/avif">
<!-- 2. Use fetchpriority to prioritize it -->
<img src="/hero.avif" alt="Hero" fetchpriority="high" width="1200" height="600">
<!-- 3. Inline critical CSS to avoid render-blocking -->
<style>
.hero { /* critical styles inlined */ }
</style>
<link rel="stylesheet" href="/full.css" media="print" onload="this.media='all'">
- Optimize server response time (target TTFB < 600ms)
- Use SSR or SSG instead of client-side rendering for above-the-fold content
- Compress and serve images in modern formats (AVIF/WebP)
- Use
font-display: swapfor web fonts
INP: Interaction to Next Paint
INP replaced FID (First Input Delay) in March 2024 because FID only measured the first interaction. INP measures the responsiveness of all interactions throughout the page lifecycle — clicks, taps, and keyboard inputs — and reports the worst one (at the 98th percentile).
An interaction's latency is measured from the moment the user interacts to the moment the browser paints the next frame showing the result. Target: under 200ms.
What causes bad INP:
- Long tasks on the main thread — JavaScript executing for 50ms+ blocks the browser from responding to input
- Heavy event handlers — Click/submit handlers that do too much synchronous work
- Excessive DOM size — Pages with 1,500+ DOM nodes are slower to update
- Layout thrashing — Reading and writing DOM layout properties in a loop forces expensive recalculations
- Third-party scripts — Analytics, ads, and widgets often run heavy code on the main thread
How to fix INP:
// Break up long tasks with yield-to-main pattern
async function handleClick() {
// Do first chunk of work
updateUI();
// Yield to let browser paint and handle other events
await new Promise(resolve => setTimeout(resolve, 0));
// Continue with non-urgent work
sendAnalytics();
prefetchNextPage();
}
// Use requestAnimationFrame for visual updates
button.addEventListener('click', () => {
requestAnimationFrame(() => {
element.classList.add('active'); // visual feedback first
});
// Heavy processing deferred
queueMicrotask(() => processData());
});
- Keep event handlers under 50ms — defer non-essential work
- Use web workers for CPU-intensive tasks (data processing, sorting)
- Reduce DOM size — virtualize long lists, remove hidden elements
- Debounce frequent interactions (scroll, resize, input events)
Tip
Use Chrome DevTools Performance panel to record interactions. Look for long tasks (marked with red triangles) during clicks and input events. The "Interactions" track shows exactly what's blocking each interaction.
CLS: Cumulative Layout Shift
CLS measures how much the visible content unexpectedly shifts during page load. You've experienced this: you're about to tap a button, an ad loads above it, and you tap something else instead. That's layout shift.
CLS is scored from 0 (no shift) to infinity. Target: under 0.1. The score accounts for both the size of the shifted area and the distance it moved.
What causes bad CLS:
- Images without dimensions — The browser doesn't know how much space to reserve until the image loads
- Ads, embeds, and iframes without reserved space — Dynamic content that injects itself into the layout
- Web fonts causing FOUT (flash of unstyled text) — Fallback font has different metrics than the web font
- Dynamically injected content — Banners, cookie notices, or content loaded after initial render that pushes other content down
- Late-loading CSS — CSS that arrives after initial render and changes element sizes
How to fix CLS:
<!-- Always set width and height on images and video -->
<img src="/photo.webp" alt="Photo" width="800" height="600">
<!-- Or use CSS aspect-ratio for responsive containers -->
<style>
.video-container {
aspect-ratio: 16 / 9;
width: 100%;
background: #f0f0f0; /* placeholder color */
}
/* Match fallback font metrics to web font */
@font-face {
font-family: 'Inter';
src: url('/inter.woff2') format('woff2');
font-display: swap;
size-adjust: 107%; /* Match x-height */
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
</style>
<!-- Reserve space for ads -->
<div style="min-height: 250px;">
<!-- Ad loads here -->
</div>
- Always specify
widthandheighton<img>and<video>elements - Reserve space for ads, embeds, and dynamic content with
min-height - Use
font-display: swapwith font metric overrides to minimize FOUT shift - Avoid inserting content above existing content (add below or use overlays instead)
- Use CSS
contain: layouton widgets that resize dynamically
Measuring Core Web Vitals accurately
There are two types of Core Web Vitals data:
- Field data (RUM) — Real user measurements collected from actual visitors via the Chrome User Experience Report (CrUX). This is what Google uses for ranking. Available in Google Search Console and PageSpeed Insights ("Discover what your real users are experiencing").
- Lab data — Simulated measurements from tools like Lighthouse, PageSpeed Insights (lab section), and WebPageTest. Useful for debugging but doesn't reflect real-world conditions.
Important: Field and lab data often differ significantly. Your Lighthouse score might be 95 but field LCP could be 4+ seconds because real users have slower devices and connections. Always prioritize field data.
Tools for measuring:
- Google Search Console — Core Web Vitals report (field data, grouped by URL pattern)
- PageSpeed Insights — Both field and lab data for a single URL
- web-vitals JS library — Add to your site for custom field data collection
- Chrome DevTools Performance panel — Lab data with detailed breakdowns
- Chrome UX Report (CrUX) API — Programmatic access to field data
// Collect real-user Core Web Vitals with the web-vitals library
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics(metric) {
// Send to your analytics endpoint
navigator.sendBeacon('/api/vitals', JSON.stringify({
name: metric.name,
value: metric.value,
delta: metric.delta,
id: metric.id,
}));
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);Frequently Asked Questions
Related Articles
Check how your website performs in this area
Get Your Growth Score