Core Web Vitals
Performance optimization for Google's Core Web Vitals - LCP, INP, CLS with 2026 thresholds.
Core Web Vitals Thresholds (2026)
Metric Good Needs Improvement Poor
LCP (Largest Contentful Paint) ≤ 2.5s ≤ 4.0s
4.0s
INP (Interaction to Next Paint) ≤ 200ms ≤ 500ms
500ms
CLS (Cumulative Layout Shift) ≤ 0.1 ≤ 0.25
0.25
Note: INP replaced FID (First Input Delay) in March 2024 as the official responsiveness metric.
Upcoming 2026 Stricter Thresholds (Q4 2025 rollout)
Metric Current Good 2026 Good
LCP ≤ 2.5s ≤ 2.0s
INP ≤ 200ms ≤ 150ms
CLS ≤ 0.1 ≤ 0.08
Plan for stricter thresholds now to maintain search rankings.
LCP Optimization
- Identify LCP Element
// Find LCP element in DevTools new PerformanceObserver((entryList) => { const entries = entryList.getEntries(); const lastEntry = entries[entries.length - 1]; console.log('LCP element:', lastEntry.element); console.log('LCP time:', lastEntry.startTime); }).observe({ type: 'largest-contentful-paint', buffered: true });
- Optimize LCP Images
// Priority loading for hero image <img src="/hero.webp" alt="Hero" fetchpriority="high" loading="eager" decoding="async" />
// Next.js Image with priority import Image from 'next/image';
<Image src="/hero.webp" alt="Hero" priority sizes="100vw" quality={85} />
- Preload Critical Resources
<!-- Preload LCP image --> <link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
<!-- Preload critical font --> <link rel="preload" as="font" href="/fonts/inter.woff2" type="font/woff2" crossorigin />
<!-- Preconnect to critical origins --> <link rel="preconnect" href="https://api.example.com" /> <link rel="dns-prefetch" href="https://analytics.example.com" />
- Server-Side Rendering
// Next.js - ensure SSR for LCP content export default async function Page() { const data = await fetchCriticalData(); return <HeroSection data={data} />; // Rendered on server }
// Avoid client-only LCP content // BAD: LCP content loaded client-side const [data, setData] = useState(null); useEffect(() => { fetchData().then(setData); }, []);
INP Optimization
- Break Up Long Tasks
// BAD: Long synchronous task (blocks main thread) function processLargeArray(items: Item[]) { items.forEach(processItem); // Blocks for entire duration }
// GOOD: Yield to main thread async function processLargeArray(items: Item[]) { for (const item of items) { processItem(item); // Yield every 50ms to allow paint if (performance.now() % 50 < 1) { await scheduler.yield?.() ?? new Promise(r => setTimeout(r, 0)); } } }
- Use Transitions for Non-Urgent Updates
import { useTransition, useDeferredValue } from 'react';
function SearchResults() { const [query, setQuery] = useState(''); const [isPending, startTransition] = useTransition();
const handleChange = (e: ChangeEvent<HTMLInputElement>) => { // Urgent: Update input immediately setQuery(e.target.value);
// Non-urgent: Defer expensive filter
startTransition(() => {
setFilteredResults(filterResults(e.target.value));
});
};
return ( <> <input value={query} onChange={handleChange} /> {isPending && <Spinner />} <ResultsList results={filteredResults} /> </> ); }
- Optimize Event Handlers
// BAD: Heavy computation in click handler <button onClick={() => { const result = heavyComputation(); // Blocks paint setResult(result); }}>Calculate</button>
// GOOD: Defer heavy work <button onClick={() => { setLoading(true); requestIdleCallback(() => { const result = heavyComputation(); setResult(result); setLoading(false); }); }}>Calculate</button>
CLS Optimization
- Reserve Space for Dynamic Content
/* Reserve space for images */ .image-container { aspect-ratio: 16 / 9; width: 100%; }
/* Reserve space for ads */ .ad-slot { min-height: 250px; }
- Explicit Dimensions
// Always set width and height <img src="/photo.jpg" width={800} height={600} alt="Photo" />
// Next.js Image handles this automatically <Image src="/photo.jpg" width={800} height={600} alt="Photo" />
// For responsive images <Image src="/photo.jpg" fill sizes="(max-width: 768px) 100vw, 50vw" />
- Avoid Layout-Shifting Fonts
/* Use font-display: optional for non-critical fonts / @font-face { font-family: 'CustomFont'; src: url('/fonts/custom.woff2') format('woff2'); font-display: optional; / Prevents flash of unstyled text */ }
/* Or use size-adjust for fallback */ @font-face { font-family: 'Fallback'; src: local('Arial'); size-adjust: 105%; ascent-override: 95%; }
- Animations That Don't Cause Layout Shift
/* BAD: Changes layout properties / .expanding { height: 0; transition: height 0.3s; } .expanding.open { height: 200px; / Causes layout shift */ }
/* GOOD: Use transform */ .expanding { transform: scaleY(0); transform-origin: top; transition: transform 0.3s; } .expanding.open { transform: scaleY(1); }
Real User Monitoring (RUM)
// web-vitals library import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics(metric: Metric) { fetch('/api/vitals', { method: 'POST', body: JSON.stringify({ name: metric.name, value: metric.value, rating: metric.rating, navigationType: metric.navigationType, }), keepalive: true, // Send even if page unloads }); }
onLCP(sendToAnalytics); onINP(sendToAnalytics); onCLS(sendToAnalytics);
Performance Budgets
// lighthouse-budget.json { "resourceSizes": [ { "resourceType": "script", "budget": 150 }, { "resourceType": "image", "budget": 300 }, { "resourceType": "total", "budget": 500 } ], "timings": [ { "metric": "largest-contentful-paint", "budget": 2500 }, { "metric": "cumulative-layout-shift", "budget": 0.1 } ] }
// webpack-budget.config.js module.exports = { performance: { maxAssetSize: 150000, // 150kb maxEntrypointSize: 250000, // 250kb hints: 'error', // Fail build if exceeded }, };
Debugging Tools
Tool Use Case
Chrome DevTools Performance Identify long tasks, layout shifts
Lighthouse Lab data, recommendations
PageSpeed Insights Field data + lab data
Web Vitals Extension Real-time vitals overlay
Chrome UX Report Real user data by origin
Quick Reference
// ✅ LCP: Preload and prioritize hero image <link rel="preload" as="image" href="/hero.webp" fetchpriority="high" /> <Image src="/hero.webp" priority fill sizes="100vw" />
// ✅ INP: Use transitions for expensive updates const [isPending, startTransition] = useTransition(); const deferredQuery = useDeferredValue(query);
// ✅ CLS: Always set dimensions, reserve space <img src="/photo.jpg" width={800} height={600} alt="Photo" /> <div className="min-h-[250px]">{/* Reserved space */}</div>
// ✅ RUM: Send metrics reliably navigator.sendBeacon('/api/vitals', JSON.stringify(metric));
// ✅ Font loading: Prevent FOUT/FOIT @font-face { font-display: optional; // or swap with size-adjust }
// ❌ NEVER: Client-side fetch for LCP content useEffect(() => { fetchHeroData().then(setData); }, []);
// ❌ NEVER: Missing dimensions on images <img src="/photo.jpg" alt="Photo" /> // Causes CLS
// ❌ NEVER: Heavy computation in event handlers onClick={() => { heavyComputation(); setResult(result); }}
Key Decisions
Decision Option A Option B Recommendation
LCP content rendering Client-side SSR/SSG SSR/SSG - Critical content must be in initial HTML
Image format JPEG/PNG WebP/AVIF WebP (AVIF for modern browsers) - 25-50% smaller
Font loading swap optional optional for non-critical, swap with fallback metrics
INP optimization Debounce useTransition useTransition - React 18+ native, better UX
Monitoring Lab only Lab + Field Lab + Field - Real user data is ground truth
Performance budget Soft warning Hard fail Hard fail in CI - Prevents regression
Anti-Patterns (FORBIDDEN)
// ❌ FORBIDDEN: LCP element rendered client-side function Hero() { const [data, setData] = useState(null); useEffect(() => { fetchHeroContent().then(setData); // LCP waits for JS + fetch! }, []); return data ? <HeroImage src={data.image} /> : <Skeleton />; }
// ❌ FORBIDDEN: Images without dimensions <img src="/photo.jpg" alt="Photo" /> // Browser can't reserve space // ✅ CORRECT: Always provide width/height <img src="/photo.jpg" width={800} height={600} alt="Photo" />
// ❌ FORBIDDEN: Lazy loading LCP image <img src="/hero.webp" loading="lazy" /> // Delays LCP! // ✅ CORRECT: Eager load with high priority <img src="/hero.webp" fetchpriority="high" loading="eager" />
// ❌ FORBIDDEN: Blocking main thread in handlers <button onClick={() => { const result = expensiveOperation(); // Blocks INP! setResult(result); }}>Calculate</button> // ✅ CORRECT: Defer heavy work <button onClick={() => { startTransition(() => { const result = expensiveOperation(); setResult(result); }); }}>Calculate</button>
// ❌ FORBIDDEN: Layout-shifting animations .sidebar { width: 0; transition: width 0.3s; // Causes layout shift! } // ✅ CORRECT: Use transform .sidebar { transform: translateX(-100%); transition: transform 0.3s; }
// ❌ FORBIDDEN: Inserting content above viewport function Banner() { const [show, setShow] = useState(false); useEffect(() => { setTimeout(() => setShow(true), 1000); // CLS! }, []); return show ? <div className="fixed top-0">Banner</div> : null; }
// ❌ FORBIDDEN: Font flash without fallback @font-face { font-family: 'Custom'; src: url('/custom.woff2'); font-display: block; // Shows nothing until font loads }
// ❌ FORBIDDEN: Only measuring in lab environment // Lab data != real user experience // Always combine Lighthouse with RUM (web-vitals library)
// ❌ FORBIDDEN: Third-party scripts blocking render <script src="https://slow-analytics.com/script.js"></script> // ✅ CORRECT: Defer or async non-critical scripts <script src="https://analytics.com/script.js" defer></script>
Related Skills
-
image-optimization
-
Comprehensive image optimization strategies
-
observability-monitoring
-
Production monitoring and alerting
-
react-server-components-framework
-
SSR/RSC for LCP optimization
-
frontend-ui-developer
-
Modern frontend patterns
-
accessibility-specialist
-
Performance intersects with a11y (skip links, focus management)
Capability Details
lcp-optimization
Keywords: LCP, largest-contentful-paint, hero, preload, priority, SSR, TTFB Solves: Slow initial render, delayed hero content, poor Time to First Byte
inp-optimization
Keywords: INP, interaction, responsiveness, long-task, transition, yield, scheduler Solves: Slow button responses, janky scrolling, blocked main thread
cls-prevention
Keywords: CLS, layout-shift, dimensions, aspect-ratio, font-display, skeleton Solves: Content jumping, image pop-in, font flash, ad insertion shifts
rum-monitoring
Keywords: RUM, web-vitals, field-data, analytics, sendBeacon, percentile Solves: Understanding real user experience, identifying regressions, alerting
performance-budgets
Keywords: budget, webpack, lighthouse-ci, bundle-size, threshold, regression Solves: Preventing performance degradation, enforcing standards, CI integration
2026-thresholds
Keywords: 2026, stricter, LCP-2.0s, INP-150ms, CLS-0.08, future-proof Solves: Preparing for Google's stricter thresholds before they become ranking factors
References
-
references/rum-setup.md
-
Complete RUM implementation
-
scripts/performance-monitoring.ts
-
Monitoring template
-
checklists/cwv-checklist.md
-
Optimization checklist
-
examples/cwv-examples.md
-
Real-world optimization examples