What Are Core Web Vitals?
Core Web Vitals are Google's user-experience metrics that directly affect search ranking. In 2024, INP replaced FID as the third metric. The three metrics you need to care about:
| Metric | What It Measures | Good | Needs Work | Poor |
|---|---|---|---|---|
| LCP | Largest Contentful Paint โ loading speed | < 2.5s | 2.5sโ4s | > 4s |
| INP | Interaction to Next Paint โ responsiveness | < 200ms | 200โ500ms | > 500ms |
| CLS | Cumulative Layout Shift โ visual stability | < 0.1 | 0.1โ0.25 | > 0.25 |
LCP: Largest Contentful Paint
LCP measures how long it takes for the largest visible element to render โ usually a hero image or main heading.
What causes bad LCP?
- Slow server response time
- Render-blocking CSS and JavaScript
- Unoptimized hero images
- Slow third-party resources
How to fix LCP
1. Preload the LCP image:
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high" />
2. Use modern image formats:
<picture>
<source srcset="/hero.avif" type="image/avif" />
<source srcset="/hero.webp" type="image/webp" />
<img src="/hero.jpg" alt="Hero" width="1200" height="600" />
</picture>
3. Remove render-blocking resources:
<!-- Defer non-critical JS -->
<script src="analytics.js" defer></script>
<!-- Load non-critical CSS asynchronously -->
<link rel="preload" href="non-critical.css" as="style" onload="this.rel='stylesheet'" />
4. Use a CDN โ server response time (TTFB) is often the biggest LCP factor. A CDN close to users can cut TTFB by 50-80%.
INP: Interaction to Next Paint
INP replaced FID in March 2024. It measures the worst-case interaction latency throughout the entire page visit โ not just the first interaction.
Every click, tap, and keypress is measured. INP is the 98th percentile of those measurements.
What causes bad INP?
- Long-running JavaScript on the main thread
- Heavy event handlers
- Synchronous DOM manipulation after interaction
- Large React re-renders triggered by clicks
How to fix INP
1. Break up long tasks:
// Bad: blocks the main thread for 500ms
function processData(items) {
items.forEach(item => heavyOperation(item))
}
// Better: yield to browser between chunks
async function processData(items) {
for (let i = 0; i < items.length; i++) {
heavyOperation(items[i])
if (i % 100 === 0) {
await scheduler.yield() // yield to browser (Chrome 115+)
// Fallback: await new Promise(r => setTimeout(r, 0))
}
}
}
2. Move work off the main thread:
// Web Workers for CPU-heavy tasks
const worker = new Worker('/heavy-worker.js')
worker.postMessage({ data: largeDataset })
worker.onmessage = (e) => updateUI(e.data)
3. Debounce expensive event handlers:
const handleSearch = debounce((value) => {
fetchResults(value) // Only fire 300ms after user stops typing
}, 300)
CLS: Cumulative Layout Shift
CLS measures how much page content unexpectedly shifts during load. A score of 0 means nothing moved. Above 0.1 means users are clicking the wrong things.
What causes bad CLS?
- Images without explicit width/height
- Ads and embeds that load and push content down
- Dynamic content injected above existing content
- Web fonts causing text to reflow
How to fix CLS
1. Always set image dimensions:
<!-- Bad: browser doesn't know space to reserve -->
<img src="/product.jpg" alt="Product" />
<!-- Good: browser reserves exact space -->
<img src="/product.jpg" alt="Product" width="400" height="300" />
2. Reserve space for dynamic content:
/* Reserve space for ads before they load */
.ad-container {
min-height: 250px;
width: 300px;
}
3. Use font-display: optional or preload fonts:
@font-face {
font-family: 'Inter';
font-display: optional; /* Don't swap fonts after initial render */
}
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />
Measuring Core Web Vitals
In the browser (real data):
import { getLCP, getINP, getCLS } from 'web-vitals'
getLCP(console.log)
getINP(console.log)
getCLS(console.log)
Tools:
- Chrome DevTools โ Performance tab โ Core Web Vitals
- PageSpeed Insights โ real-world data from Chrome users
- Lighthouse CI โ automate in your pipeline
Key Takeaways
- LCP: preload your hero image, use AVIF/WebP, add a CDN
- INP: break up long tasks, defer non-critical JS, debounce handlers
- CLS: always set image dimensions, reserve space for dynamic content
- Measure with real user data (PageSpeed Insights), not just lab tools (Lighthouse)
- A CDN alone often fixes 60% of LCP issues โ it's the highest-leverage change