Core Web Vitals: Measure and Optimize User Experience
Core Web Vitals directly impact user experience and SEO. Learn how to measure and optimize each metric.
Quick Decision Guide
Quick Optimization Guide:
LCP < 2.5s: Optimize images (WebP, lazy load), use CDN, preload critical resources, reduce server response time.
INP < 200ms: Reduce main-thread contention, break up long tasks, defer non-critical JS, and minimize third-party script impact.
CLS < 0.1: Set width/height on images/videos, reserve space for ads, avoid inserting content above existing content.
Measure: Use Lighthouse, PageSpeed Insights, or web-vitals library to track real-user metrics (RUM).
Largest Contentful Paint (LCP)
What is LCP?
LCP measures loading performance by tracking when the largest visible element renders.
Common LCP Elements
Measuring LCP
import { getLCP } from 'web-vitals';
getLCP((metric) => {
console.log('LCP:', metric.value, 'ms');
// Good: < 2500ms
// Needs improvement: 2500-4000ms
// Poor: > 4000ms
});Optimization Strategies
1. Optimize Images
<!-- Use modern formats -->
<picture>
<source srcset="hero.webp" type="image/webp">
<source srcset="hero.avif" type="image/avif">
<img src="hero.jpg" alt="Hero">
</picture>
<!-- Preload LCP image -->
<link rel="preload" as="image" href="hero.jpg">2. Use CDN
// Instead of: /images/hero.jpg
// Use CDN: https://cdn.example.com/hero.jpg
<img src="https://cdn.example.com/hero.jpg" />3. Reduce Server Response Time (TTFB)
// Implement caching
app.use((req, res, next) => {
res.set('Cache-Control', 'public, max-age=31536000');
next();
});
// Use HTTP/2 or HTTP/3
// Enable compression (gzip, brotli)4. Remove Render-Blocking Resources
<!-- Defer non-critical CSS -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<!-- Inline critical CSS -->
<style>
/* Above-the-fold styles */
</style>Interaction to Next Paint (INP)
What is INP?
INP measures interactivity by tracking the latency of user interactions (click, tap, key press) and reporting a high-percentile interaction delay over the page lifecycle.
> FID is deprecated as a Core Web Vital. Use INP for interactivity evaluation.
Measuring INP
import { getINP } from 'web-vitals';
getINP((metric) => {
console.log('INP:', metric.value, 'ms');
// Good: < 200ms
// Needs improvement: 200-500ms
// Poor: > 500ms
});Optimization Strategies
1. Break Up Long Tasks
// ❌ Bad: Long blocking task
function processData(data) {
for (let i = 0; i < 10000; i++) {
// Heavy computation
}
}
// ✅ Good: Chunked processing
async function processData(data) {
const chunkSize = 100;
for (let i = 0; i < data.length; i += chunkSize) {
await new Promise(resolve => setTimeout(resolve, 0));
// Process chunk
}
}2. Use Web Workers
// Offload heavy computation
const worker = new Worker('heavy-computation.js');
worker.postMessage({ data: largeDataset });
worker.onmessage = (e) => {
console.log('Result:', e.data);
};3. Defer Non-Critical JavaScript
<!-- Defer analytics -->
<script defer src="analytics.js"></script>
<!-- Load chat widget after interaction -->
<script>
document.addEventListener('click', () => {
import('./chat-widget.js');
}, { once: true });
</script>4. Reduce Interaction Handler Work
button.addEventListener('click', () => {
// Keep event handlers short and schedule non-urgent work
requestAnimationFrame(() => updateUI());
setTimeout(() => runNonCriticalWork(), 0);
});Cumulative Layout Shift (CLS)
What is CLS?
CLS measures visual stability by tracking unexpected layout shifts during page load.
Measuring CLS
import { getCLS } from 'web-vitals';
getCLS((metric) => {
console.log('CLS:', metric.value);
// Good: < 0.1
// Needs improvement: 0.1-0.25
// Poor: > 0.25
});Common Causes of CLS
1. Images without dimensions
2. Ads/embeds/iframes without reserved space
3. Dynamically injected content
4. Web fonts causing FOIT/FOUT
Optimization Strategies
1. Set Dimensions on Images
<!-- ❌ Bad: No dimensions -->
<img src="photo.jpg" alt="Photo">
<!-- ✅ Good: Fixed dimensions -->
<img src="photo.jpg" width="800" height="600" alt="Photo">
<!-- ✅ Better: Aspect ratio -->
<img
src="photo.jpg"
style="aspect-ratio: 16/9; width: 100%;"
alt="Photo"
>2. Reserve Space for Ads
.ad-container {
min-height: 250px; /* Reserve space */
background: #f0f0f0; /* Placeholder color */
}3. Avoid Inserting Content Above
// ❌ Bad: Insert above
document.body.prepend(banner);
// ✅ Good: Append or use fixed positioning
document.body.append(banner);
// or
banner.style.position = 'fixed';4. Optimize Web Fonts
/* Prevent layout shift from font loading */
@font-face {
font-family: 'Custom Font';
src: url('font.woff2') format('woff2');
font-display: optional; /* Use system font if not loaded quickly */
}
/* Or use fallback with similar metrics */
body {
font-family: 'Custom Font', Arial, sans-serif;
}Measuring in Production
web-vitals Library
import { getCLS, getINP, getLCP, getFCP, getTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
// Send to your analytics endpoint
fetch('/analytics', {
method: 'POST',
body: JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
delta: metric.delta
})
});
}
getCLS(sendToAnalytics);
getINP(sendToAnalytics);
getLCP(sendToAnalytics);
getFCP(sendToAnalytics);
getTTFB(sendToAnalytics);Chrome User Experience Report (CrUX)
// Query CrUX API
async function getCrUXData(url) {
const response = await fetch(
`https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=API_KEY`,
{
method: 'POST',
body: JSON.stringify({ url })
}
);
const data = await response.json();
console.log('CrUX data:', data);
}Performance Observer
// Low-level API
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'largest-contentful-paint') {
console.log('LCP:', entry.renderTime || entry.loadTime);
}
// INP is best measured via the web-vitals library; first-input is legacy/FID-oriented
if (entry.entryType === 'layout-shift' && !entry.hadRecentInput) {
console.log('CLS:', entry.value);
}
}
});
observer.observe({
entryTypes: ['largest-contentful-paint', 'layout-shift']
});Best Practices
Testing Tools
1. Lighthouse: Automated audits in Chrome DevTools
2. PageSpeed Insights: Google's tool with real-user data
3. WebPageTest: Detailed performance analysis
4. Chrome DevTools: Performance panel
Optimization Checklist
LCP
INP
CLS
Monitoring
// Set up Real User Monitoring (RUM)
import { getCLS, getINP, getLCP } from 'web-vitals';
const vitalsUrl = 'https://analytics.example.com/vitals';
function sendToAnalytics({ name, delta, value, id }) {
const body = JSON.stringify({ name, delta, value, id });
// Use sendBeacon for reliability
if (navigator.sendBeacon) {
navigator.sendBeacon(vitalsUrl, body);
} else {
fetch(vitalsUrl, { body, method: 'POST', keepalive: true });
}
}
getCLS(sendToAnalytics);
getINP(sendToAnalytics);
getLCP(sendToAnalytics);Performance Budget
// Set thresholds in your CI/CD
const budgets = {
LCP: 2500, // ms
INP: 200, // ms
CLS: 0.1 // score
};
// Fail build if metrics exceed budget
if (metrics.LCP > budgets.LCP) {
throw new Error('LCP exceeds budget');
}