Core Web Vitals are Google's metrics for measuring real-world user experience. This guide covers practical optimization techniques for each vital metric.
Understanding the Metrics#
Largest Contentful Paint (LCP)#
Measures loading performance. Target: under 2.5 seconds.
1// Measure LCP
2new PerformanceObserver((entryList) => {
3 for (const entry of entryList.getEntries()) {
4 console.log('LCP:', entry.startTime, entry.element);
5 }
6}).observe({ type: 'largest-contentful-paint', buffered: true });Interaction to Next Paint (INP)#
Measures responsiveness. Target: under 200 milliseconds.
1// Measure INP
2new PerformanceObserver((entryList) => {
3 for (const entry of entryList.getEntries()) {
4 if (entry.interactionId) {
5 console.log('Interaction:', entry.duration, entry.name);
6 }
7 }
8}).observe({ type: 'event', buffered: true, durationThreshold: 16 });Cumulative Layout Shift (CLS)#
Measures visual stability. Target: under 0.1.
1// Measure CLS
2let clsValue = 0;
3new PerformanceObserver((entryList) => {
4 for (const entry of entryList.getEntries()) {
5 if (!entry.hadRecentInput) {
6 clsValue += entry.value;
7 console.log('CLS:', clsValue);
8 }
9 }
10}).observe({ type: 'layout-shift', buffered: true });Optimizing LCP#
1. Optimize Critical Images#
1<!-- Preload LCP image -->
2<link
3 rel="preload"
4 as="image"
5 href="/hero-image.webp"
6 fetchpriority="high"
7>
8
9<!-- Use modern formats with fallbacks -->
10<picture>
11 <source srcset="/hero.avif" type="image/avif">
12 <source srcset="/hero.webp" type="image/webp">
13 <img
14 src="/hero.jpg"
15 alt="Hero image"
16 width="1200"
17 height="600"
18 fetchpriority="high"
19 decoding="async"
20 >
21</picture>2. Eliminate Render-Blocking Resources#
1<!-- Defer non-critical CSS -->
2<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
3<noscript><link rel="stylesheet" href="/styles.css"></noscript>
4
5<!-- Inline critical CSS -->
6<style>
7 /* Critical above-the-fold styles */
8 .hero { ... }
9 .nav { ... }
10</style>3. Optimize Fonts#
1<!-- Preload critical fonts -->
2<link
3 rel="preload"
4 href="/fonts/inter-var.woff2"
5 as="font"
6 type="font/woff2"
7 crossorigin
8>
9
10<style>
11 @font-face {
12 font-family: 'Inter';
13 src: url('/fonts/inter-var.woff2') format('woff2');
14 font-display: swap;
15 font-weight: 100 900;
16 }
17</style>4. Server-Side Rendering#
1// Next.js: Pre-render critical content
2export default function HomePage({ heroData }) {
3 return (
4 <main>
5 <HeroSection data={heroData} />
6 <Suspense fallback={<Skeleton />}>
7 <DynamicContent />
8 </Suspense>
9 </main>
10 );
11}
12
13export async function getStaticProps() {
14 const heroData = await fetchHeroData();
15 return {
16 props: { heroData },
17 revalidate: 3600,
18 };
19}5. CDN and Caching#
1// next.config.js
2module.exports = {
3 images: {
4 domains: ['cdn.example.com'],
5 formats: ['image/avif', 'image/webp'],
6 },
7 async headers() {
8 return [
9 {
10 source: '/:all*(svg|jpg|png|webp|avif)',
11 headers: [
12 {
13 key: 'Cache-Control',
14 value: 'public, max-age=31536000, immutable',
15 },
16 ],
17 },
18 ];
19 },
20};Optimizing INP#
1. Break Up Long Tasks#
1// Bad: Long blocking task
2function processLargeDataset(data) {
3 data.forEach(item => heavyOperation(item));
4}
5
6// Good: Yield to main thread
7async function processLargeDataset(data) {
8 const CHUNK_SIZE = 50;
9
10 for (let i = 0; i < data.length; i += CHUNK_SIZE) {
11 const chunk = data.slice(i, i + CHUNK_SIZE);
12 chunk.forEach(item => heavyOperation(item));
13
14 // Yield to main thread
15 await scheduler.yield?.() ??
16 new Promise(r => setTimeout(r, 0));
17 }
18}2. Debounce User Input#
1// Debounce search input
2function debounce(fn, delay) {
3 let timeoutId;
4 return (...args) => {
5 clearTimeout(timeoutId);
6 timeoutId = setTimeout(() => fn(...args), delay);
7 };
8}
9
10const searchInput = document.getElementById('search');
11const debouncedSearch = debounce(performSearch, 300);
12
13searchInput.addEventListener('input', (e) => {
14 debouncedSearch(e.target.value);
15});3. Optimize Event Handlers#
1// Bad: Heavy computation in click handler
2<button onClick={() => {
3 const result = expensiveCalculation(data);
4 setResult(result);
5}}>
6 Calculate
7</button>
8
9// Good: Defer to next frame
10<button onClick={() => {
11 requestAnimationFrame(() => {
12 const result = expensiveCalculation(data);
13 setResult(result);
14 });
15}}>
16 Calculate
17</button>
18
19// Better: Use Web Worker for heavy computation
20<button onClick={() => {
21 worker.postMessage({ type: 'calculate', data });
22}}>
23 Calculate
24</button>4. Use CSS for Animations#
1/* Bad: JavaScript animation */
2/* element.style.transform = `translateX(${x}px)` in rAF */
3
4/* Good: CSS transitions */
5.element {
6 transition: transform 0.3s ease-out;
7 will-change: transform;
8}
9
10.element.moved {
11 transform: translateX(100px);
12}
13
14/* Good: CSS animations */
15@keyframes slideIn {
16 from { transform: translateX(-100%); }
17 to { transform: translateX(0); }
18}
19
20.element {
21 animation: slideIn 0.3s ease-out;
22}5. Virtualize Long Lists#
1import { FixedSizeList } from 'react-window';
2
3function VirtualizedList({ items }) {
4 const Row = ({ index, style }) => (
5 <div style={style}>
6 {items[index].name}
7 </div>
8 );
9
10 return (
11 <FixedSizeList
12 height={600}
13 width="100%"
14 itemCount={items.length}
15 itemSize={50}
16 >
17 {Row}
18 </FixedSizeList>
19 );
20}Optimizing CLS#
1. Reserve Space for Images#
1<!-- Always include dimensions -->
2<img
3 src="/product.jpg"
4 alt="Product"
5 width="400"
6 height="300"
7>
8
9<!-- Or use aspect-ratio CSS -->
10<style>
11 .image-container {
12 aspect-ratio: 16 / 9;
13 width: 100%;
14 }
15
16 .image-container img {
17 width: 100%;
18 height: 100%;
19 object-fit: cover;
20 }
21</style>2. Reserve Space for Ads#
1.ad-container {
2 min-height: 250px;
3 background: #f0f0f0;
4}
5
6/* Specific ad sizes */
7.ad-leaderboard { min-height: 90px; }
8.ad-rectangle { min-height: 250px; }
9.ad-skyscraper { min-height: 600px; }3. Avoid Dynamic Content Insertion#
1// Bad: Content shifts when loaded
2function Comments() {
3 const [comments, setComments] = useState(null);
4
5 if (!comments) return null; // Nothing rendered
6
7 return <CommentList comments={comments} />;
8}
9
10// Good: Reserve space with skeleton
11function Comments() {
12 const [comments, setComments] = useState(null);
13
14 if (!comments) {
15 return <CommentsSkeleton count={5} />;
16 }
17
18 return <CommentList comments={comments} />;
19}4. Use CSS Containment#
1.card {
2 contain: layout;
3}
4
5.sidebar {
6 contain: strict;
7 width: 300px;
8 height: 100vh;
9}5. Handle Web Fonts#
1/* Prevent FOUT causing layout shift */
2@font-face {
3 font-family: 'Custom Font';
4 src: url('/font.woff2') format('woff2');
5 font-display: optional; /* Best for CLS */
6 /* or use font-display: swap with size-adjust */
7}
8
9/* Match fallback metrics */
10@font-face {
11 font-family: 'Custom Font';
12 src: url('/font.woff2') format('woff2');
13 font-display: swap;
14 size-adjust: 105%;
15 ascent-override: 90%;
16 descent-override: 20%;
17}Measuring in Production#
Web Vitals Library#
1import { onCLS, onINP, onLCP } from 'web-vitals';
2
3function sendToAnalytics({ name, value, id }) {
4 fetch('/analytics', {
5 method: 'POST',
6 body: JSON.stringify({ name, value, id }),
7 });
8}
9
10onCLS(sendToAnalytics);
11onINP(sendToAnalytics);
12onLCP(sendToAnalytics);Next.js Built-in Reporting#
1// pages/_app.js
2export function reportWebVitals(metric) {
3 switch (metric.name) {
4 case 'LCP':
5 case 'INP':
6 case 'CLS':
7 console.log(metric.name, metric.value);
8 // Send to analytics
9 break;
10 }
11}Tools for Analysis#
- Chrome DevTools: Performance panel, Lighthouse
- PageSpeed Insights: Lab and field data
- Chrome UX Report: Real user data
- WebPageTest: Detailed waterfall analysis
- Search Console: Core Web Vitals report
Quick Wins Checklist#
- Preload LCP image
- Inline critical CSS
- Use font-display: swap
- Add width/height to images
- Defer non-critical JavaScript
- Enable compression (Brotli/gzip)
- Use CDN for static assets
- Implement caching headers
- Avoid layout-triggering animations
- Reserve space for dynamic content
Conclusion#
Core Web Vitals directly impact user experience and search rankings. Focus on the biggest opportunities first—usually image optimization and render-blocking resources for LCP, long tasks for INP, and missing dimensions for CLS. Measure continuously in production to catch regressions.