Core Web Vitals are the three metrics Google considers most important for user experience. They measure loading performance, interactivity, and visual stability. Understanding them deeply is essential for any performance testing strategy.
Largest Contentful Paint (LCP)
LCP measures how long it takes for the largest visible element to render. This is usually a hero image, a large heading, or a video thumbnail — whatever the user perceives as the "main content" of the page.
What Good Looks Like
| Rating | LCP |
|---|---|
| Good | Under 2.5s |
| Needs Improvement | 2.5s – 4.0s |
| Poor | Over 4.0s |
Common Causes of Poor LCP
- Slow server response: High TTFB delays everything. Optimize your backend or use a CDN.
- Render-blocking resources: Large CSS or JavaScript files that block rendering. Use
asyncordeferon scripts. - Unoptimized images: Large hero images without proper sizing or modern formats. Use WebP/AVIF and
loading="eager"for above-the-fold images. - Client-side rendering: SPAs that require JavaScript to render any content. Use SSR or SSG instead.
How to Measure LCP
// Using the Performance Observer API
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.startTime, 'ms');
console.log('Element:', lastEntry.element);
}).observe({ type: 'largest-contentful-paint', buffered: true });Interaction to Next Paint (INP)
INP replaced First Input Delay (FID) in March 2024. While FID only measured the first interaction, INP measures the responsiveness of all interactions throughout the page's lifecycle and reports the worst one.
An "interaction" is a click, tap, or keyboard input. INP measures the time from when the user interacts to when the browser paints the next frame in response.
What Good Looks Like
| Rating | INP |
|---|---|
| Good | Under 200ms |
| Needs Improvement | 200ms – 500ms |
| Poor | Over 500ms |
Common Causes of Poor INP
- Long tasks on the main thread: JavaScript functions that take over 50ms block the browser from responding. Break them into smaller chunks.
- Heavy event handlers: Complex logic in click or input handlers. Debounce expensive operations.
- Large DOM size: Browsers take longer to update the layout when the DOM has thousands of nodes.
- Third-party scripts: Analytics, ads, and chat widgets often block the main thread.
Improving INP
// Bad: Heavy computation blocks the main thread
button.addEventListener('click', () => {
const result = heavyComputation(data); // 300ms
updateUI(result);
});
// Good: Yield to the browser between tasks
button.addEventListener('click', async () => {
// Show immediate feedback
showSpinner();
// Yield to let the browser paint
await new Promise((resolve) => setTimeout(resolve, 0));
const result = heavyComputation(data);
updateUI(result);
});Cumulative Layout Shift (CLS)
CLS measures how much the page layout shifts unexpectedly during its lifetime. A layout shift happens when a visible element changes position without user interaction — for example, when an image loads and pushes text down, or a banner inserts itself above the content.
What Good Looks Like
| Rating | CLS |
|---|---|
| Good | Under 0.1 |
| Needs Improvement | 0.1 – 0.25 |
| Poor | Over 0.25 |
Common Causes of Poor CLS
- Images without dimensions: Always set
widthandheightattributes so the browser reserves space. - Dynamically injected content: Banners, ads, or cookie notices that push content down. Reserve space for them in advance.
- Web fonts causing FOUT: When a web font loads and changes text size. Use
font-display: swapwith fallback fonts that have similar metrics. - Late-loading embeds: Iframes, videos, or widgets that resize after loading.
Preventing CLS
<!-- Always set dimensions on images -->
<img src="hero.jpg" width="1200" height="600" alt="Hero" />
<!-- Reserve space for ads -->
<div style="min-height: 250px;">
<!-- Ad loads here -->
</div>Measuring Core Web Vitals in the Field
Lab tools like Lighthouse give you controlled measurements. Field data from real users is equally important:
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP((metric) => sendToAnalytics('LCP', metric.value));
onINP((metric) => sendToAnalytics('INP', metric.value));
onCLS((metric) => sendToAnalytics('CLS', metric.value));The web-vitals library from Google provides a simple API to capture real user metrics and send them to your analytics platform. This field data is what Google uses for search ranking.
Key Takeaways
- LCP measures loading (target: under 2.5s), INP measures interactivity (target: under 200ms), CLS measures stability (target: under 0.1).
- Optimize images, reduce render-blocking resources, and use SSR to improve LCP.
- Break long tasks and minimize main-thread blocking to improve INP.
- Always set image dimensions and reserve space for dynamic content to prevent CLS.