Back to Blog
Core Web VitalsPerformanceSEOWeb Development

Core Web Vitals: A Complete Performance Optimization Guide

Master Core Web Vitals to improve user experience and SEO. Learn practical techniques for LCP, INP, and CLS optimization.

B
Bootspring Team
Engineering
February 26, 2026
6 min read

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#

  1. Chrome DevTools: Performance panel, Lighthouse
  2. PageSpeed Insights: Lab and field data
  3. Chrome UX Report: Real user data
  4. WebPageTest: Detailed waterfall analysis
  5. 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.

Share this article

Help spread the word about Bootspring