Core Web Vitals 2026: The Three Metrics That Matter
Core Web Vitals are the performance metrics Google uses as ranking signals. In 2026, the three metrics are: Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS). INP replaced First Input Delay (FID) in March 2024 — this is a critical change for interview prep.
LCP measures loading speed (how fast the largest piece of content appears). INP measures responsiveness (how quickly the page responds to ALL user interactions, not just the first). CLS measures visual stability (how much the layout shifts during loading).
The thresholds: LCP < 2.5s (good), INP < 200ms (good), CLS < 0.1 (good). These are measured at the 75th percentile of all page views — meaning 75% of your users must have a good experience, not just the median.
Key Takeaways
LCP: Fixing the Largest Contentful Paint
LCP typically refers to the largest image, video poster, or text block visible in the viewport. The most common LCP culprits are: hero images loaded lazily (they should be eager-loaded), web fonts blocking text rendering, server response time (TTFB), and render-blocking CSS/JS.
The #1 LCP optimization most developers miss: use fetchpriority="high" on the LCP image. This tells the browser to prioritize downloading it above other resources. Combined with preloading, it can reduce LCP by 20-40%.
For text-based LCP elements, use font-display: swap in your @font-face declarations to prevent invisible text while custom fonts load.
<!-- ✅ LCP Image Optimization --> <!-- 1. Preload the LCP image in <head> --> <link rel="preload" as="image" href="/hero-image.webp" fetchpriority="high" type="image/webp" /> <!-- 2. Eager load with fetchpriority="high" --> <img src="/hero-image.webp" alt="Hero banner" width="1200" height="600" fetchpriority="high" loading="eager" decoding="async" /> <!-- ❌ Common mistake: lazy loading the LCP image --> <img src="/hero.webp" loading="lazy" /> <!-- NEVER lazy-load the LCP element! --> <!-- ✅ Next.js: priority prop on next/image --> import Image from 'next/image'; <Image src="/hero.webp" alt="Hero" width={1200} height={600} priority // Sets fetchpriority="high" + eager loading />
Key Takeaways
INP: The New Responsiveness Metric
INP (Interaction to Next Paint) replaced FID as a Core Web Vital in March 2024. While FID only measured the delay before the FIRST interaction, INP measures the worst-case response time across ALL interactions during the entire page visit.
INP counts three phases: Input Delay (time before event handler runs — blocked by main thread work), Processing Time (event handler execution time), and Presentation Delay (time to calculate style/layout and paint the next frame).
The biggest INP killers are: long JavaScript tasks that block the main thread, expensive React re-renders, synchronous layout calculations (forced reflows), and large DOM trees that slow down style calculations.
// ❌ Bad: Long task blocks main thread → poor INP button.addEventListener('click', () => { // Synchronous heavy computation — blocks UI for 500ms const result = heavyComputation(data); // 500ms blocking! updateUI(result); }); // ✅ Fix 1: Break up long tasks with scheduler.yield() button.addEventListener('click', async () => { const chunk1 = processFirstHalf(data); await scheduler.yield(); // Give browser a chance to paint const chunk2 = processSecondHalf(data); await scheduler.yield(); updateUI([...chunk1, ...chunk2]); }); // ✅ Fix 2: Use requestIdleCallback for non-urgent work button.addEventListener('click', () => { // Immediate visual feedback showLoadingSpinner(); // Defer heavy work to idle time requestIdleCallback(() => { const result = heavyComputation(data); updateUI(result); hideLoadingSpinner(); }); }); // ✅ Fix 3: Move heavy computation to a Web Worker const worker = new Worker('/compute-worker.js'); button.addEventListener('click', () => { showLoadingSpinner(); worker.postMessage({ data }); }); worker.onmessage = (e) => { updateUI(e.data.result); hideLoadingSpinner(); };
CLS: Eliminating Layout Shifts
CLS measures how much your page layout shifts unexpectedly. Every time an element changes position after it has been painted to the screen (without user interaction triggering it), it contributes to the CLS score. A score above 0.1 is considered poor.
The top CLS causes: images without width/height attributes, dynamically injected content (ads, banners), web fonts causing text reflow (FOUT), and late-loading CSS that changes element sizes.
The fix is always the same: reserve space for content before it loads. Set explicit width and height on images and videos, use aspect-ratio CSS, and use min-height for dynamic content areas.
/* ✅ Reserve space for images — prevents layout shift */ img, video { /* Modern approach: aspect-ratio from HTML attributes */ aspect-ratio: attr(width) / attr(height); width: 100%; height: auto; } /* ✅ Skeleton for dynamic content — reserves exact space */ .ad-slot { min-height: 250px; /* Reserve space before ad loads */ background: #f0f0f0; contain: layout; /* Prevents this element from shifting others */ } /* ✅ Font-display: optional — prevents ANY text reflow */ @font-face { font-family: 'CustomFont'; src: url('/fonts/custom.woff2') format('woff2'); font-display: optional; /* If font isn't cached, use system font — zero CLS */ } /* ✅ CSS containment — limits layout recalculation scope */ .card { contain: layout style paint; /* Changes inside .card can't affect outside layout */ content-visibility: auto; /* Skip rendering for off-screen cards */ }
Key Takeaways
View Transitions API: Smooth Page Navigations
The View Transitions API (stable in Chrome, Safari 18+) enables smooth animated transitions between page states — both for single-page apps (SPA) and multi-page apps (MPA). It creates a snapshot of the current DOM, applies your changes, then animates between the old and new states.
This replaces the need for complex animation libraries for page transitions. The browser handles snapshotting, cross-fading, and even morphing shared elements between pages. It works beautifully with Next.js App Router navigation.
For MPAs, the API works across full page navigations with a few lines of CSS. For SPAs, you wrap state changes in document.startViewTransition() to animate between states.
// SPA: Animate between states async function navigateTo(url) { // Check browser support if (!document.startViewTransition) { return updateDOM(url); // Fallback: no animation } const transition = document.startViewTransition(async () => { await updateDOM(url); }); // Wait for transition to complete await transition.finished; } /* CSS: Define transition animations */ /* Default cross-fade (works out of the box) */ ::view-transition-old(root) { animation: fade-out 0.3s ease-out; } ::view-transition-new(root) { animation: fade-in 0.3s ease-in; } /* Shared element transition — morphs between pages */ /* Old page */ .blog-card { view-transition-name: blog-hero; } /* New page */ .blog-hero-image { view-transition-name: blog-hero; } /* The browser automatically morphs blog-card → blog-hero-image */ ::view-transition-group(blog-hero) { animation-duration: 0.4s; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
Speculation Rules API: Instant Navigations
The Speculation Rules API (Chrome 121+) lets browsers prefetch or prerender pages that users are likely to navigate to. When the user actually clicks, the page loads instantly — literally 0ms perceived load time because it was already rendered in a hidden tab.
This is different from <link rel="prefetch"> which only downloads the HTML. Speculation Rules with "prerender" actually executes JavaScript and renders the page in a hidden browsing context, making it instant on navigation.
You can define rules based on URL patterns, CSS selectors (e.g., "prerender all links with class .nav-link"), or eagerness levels (immediate, eager, moderate, conservative).
<!-- Speculation Rules: Prerender pages for instant navigation --> <script type="speculationrules"> { "prerender": [ { "where": { "href_matches": "/blog/*" }, "eagerness": "moderate" } ], "prefetch": [ { "where": { "selector_matches": ".nav-link" }, "eagerness": "eager" }, { "urls": ["/about", "/blog", "/contact"] } ] } </script> <!-- Eagerness levels: - "immediate": Speculate as soon as rules are observed - "eager": Speculate at the earliest opportunity (same as immediate currently) - "moderate": Speculate on pointer hover (200ms delay) - "conservative": Speculate only on pointer down (mouse click start) --> <!-- Next.js: Use next/link with prefetch --> import Link from 'next/link'; // Next.js automatically prefetches visible Link destinations <Link href="/blog" prefetch={true}>Blog</Link>
Key Takeaways
Image Optimization: The Biggest Performance Win
Images typically account for 50-70% of total page weight. Optimizing them is the single biggest performance win for most websites. The modern stack: WebP for broad compatibility, AVIF for maximum compression, and responsive srcset for serving the right size.
The key rule: never serve an image larger than it appears on screen. A 4000px hero image displayed at 1200px wastes 90% of the bytes. Use srcset with sizes attribute to let the browser pick the best size for the user's device.
For Next.js, next/image handles all of this automatically: responsive sizes, WebP/AVIF conversion, lazy loading, and blur placeholders.
<!-- ✅ Responsive images with srcset and sizes --> <img src="/blog/hero-800.webp" srcset=" /blog/hero-400.webp 400w, /blog/hero-800.webp 800w, /blog/hero-1200.webp 1200w, /blog/hero-1600.webp 1600w " sizes="(max-width: 640px) 100vw, (max-width: 1024px) 80vw, 1200px" alt="Blog hero image" width="1200" height="600" loading="lazy" decoding="async" /> <!-- ✅ AVIF with WebP and JPEG fallbacks --> <picture> <source srcset="/hero.avif" type="image/avif" /> <source srcset="/hero.webp" type="image/webp" /> <img src="/hero.jpg" alt="Hero" width="1200" height="600" /> </picture> <!-- ✅ Next.js Image — handles everything automatically --> import Image from 'next/image'; <Image src="/blog/hero.jpg" alt="Blog hero" width={1200} height={600} sizes="(max-width: 640px) 100vw, 80vw" placeholder="blur" blurDataURL="data:image/jpeg;base64,..." />
Key Takeaways
JavaScript Bundle Optimization
After images, JavaScript is the biggest performance bottleneck. Every KB of JS must be downloaded, parsed, compiled, and executed — making it far more expensive than equivalent bytes of HTML, CSS, or images.
The target for production: < 100KB of JavaScript for the initial page load (first load JS in Next.js). Anything above 200KB should raise red flags.
Key strategies: code splitting (load only what the current route needs), tree shaking (remove unused exports), dynamic imports (lazy-load heavy components), and replacing heavy libraries with lighter alternatives.
// ✅ Dynamic import — lazy load heavy components import dynamic from 'next/dynamic'; const CodeEditor = dynamic(() => import('./CodeEditor'), { loading: () => <div className="editor-skeleton" />, ssr: false, // Don't server-render (uses browser APIs) }); // ✅ Route-based code splitting (automatic in Next.js App Router) // Each page.tsx is automatically a separate bundle // ✅ Replace heavy libraries with lighter alternatives // ❌ moment.js (67KB gzipped) → ✅ date-fns (tree-shakeable, ~3KB per function) // ❌ lodash (71KB) → ✅ lodash-es (tree-shakeable) or native methods // ❌ axios (13KB) → ✅ native fetch() (0KB — built into browser & Node 22) // ✅ Analyze your bundle // next.config.ts const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }); module.exports = withBundleAnalyzer(nextConfig); // Run: ANALYZE=true next build
Measuring Performance: Real User Monitoring
Lab tools (Lighthouse, PageSpeed Insights) test in controlled conditions. Field data (Chrome UX Report, web-vitals library) measures real users on real devices and networks. Google uses FIELD data for rankings, not lab data.
Always measure with the web-vitals library in production to get real Core Web Vitals data from your actual users. Send it to your analytics service to identify pages and interactions that need optimization.
Use Chrome DevTools Performance panel to diagnose issues: look for Long Tasks (>50ms) blocking the main thread, layout shifts, and slow network requests.
// ✅ Measure real user Core Web Vitals import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals'; function sendToAnalytics(metric) { // Send to your analytics service const body = JSON.stringify({ name: metric.name, value: metric.value, rating: metric.rating, // 'good', 'needs-improvement', 'poor' delta: metric.delta, id: metric.id, page: window.location.pathname, // Attribution data — tells you WHAT caused the issue ...(metric.attribution && { element: metric.attribution.element, largestShiftTarget: metric.attribution.largestShiftTarget, }), }); // Use sendBeacon for reliability (fires even on page unload) navigator.sendBeacon('/api/analytics', body); } onLCP(sendToAnalytics); onINP(sendToAnalytics); onCLS(sendToAnalytics); onFCP(sendToAnalytics); onTTFB(sendToAnalytics);
Key Takeaways
Key Takeaways
Web performance in 2026 revolves around three Core Web Vitals (LCP, INP, CLS), modern APIs (View Transitions, Speculation Rules), and fundamental practices (image optimization, bundle splitting, real user monitoring).
The highest-impact optimizations: fetchpriority="high" on LCP images, breaking long tasks with scheduler.yield() for INP, setting width/height on all images for CLS, using next/image for automatic optimization, and Speculation Rules for instant navigations.
For interviews, understand: why INP replaced FID, how View Transitions work, what Speculation Rules prerender vs prefetch, and how to use the web-vitals library for real user monitoring.