The Frontend Performance Playbook: Core Web Vitals in 2025
INP replaced FID. New CWV thresholds are live. Here's how to pass all three signals on a modern Next.js stack.
Core Web Vitals are now a confirmed Google ranking signal, and the bar keeps moving. INP (Interaction to Next Paint) replaced FID in March 2024 and is significantly harder to pass. If your CWV scores haven't been reviewed in 2025, the chances are you're failing at least one — and losing both rankings and conversions.
The three signals and their 2025 thresholds
LCP (Largest Contentful Paint): under 2.5 seconds. INP (Interaction to Next Paint): under 200 milliseconds. CLS (Cumulative Layout Shift): under 0.1. These are P75 thresholds — 75% of your real users need to hit them, not just your lab measurements. Passing in Lighthouse and failing in CrUX is the most common disconnect teams don't diagnose until their rankings drop.
Fixing LCP: the hero image problem
LCP is almost always the hero image or heading. The common failure modes: the image is lazy-loaded (it shouldn't be — hero images should be `priority` in Next.js Image), the image is not preloaded in `<head>`, the server response is slow, or the image is unnecessarily large. Fix order: preload the LCP resource, serve it from a CDN, use modern formats (AVIF > WebP > JPEG), and ensure the server TTFB is under 600ms.
INP: the 200ms budget
INP measures the worst interaction delay across the page session. Every click, keystroke, or tap must produce a visual response within 200ms. The most common culprits: long JavaScript tasks blocking the main thread, React re-renders triggered by global state updates, third-party scripts (analytics, ads, chat widgets) that execute on the main thread, and event handlers that do too much synchronous work. Use Chrome DevTools' Performance panel to find interactions with high INP values — they're reported with the offending task highlighted.
Breaking up long tasks
Any JavaScript task over 50ms is a 'long task' that can delay input response. The fix is task yielding — breaking long tasks into smaller chunks using `scheduler.yield()` (or `setTimeout(0)` as a fallback). In React, use `useTransition` to mark non-urgent state updates, `useDeferredValue` for derived state, and `startTransition` to defer expensive renders. These primitives existed before INP — INP finally gives teams a measurable reason to use them.
CLS: the layout stability problem
CLS failures almost always come from images without dimensions, dynamic content inserted above existing content (banners, cookie notices, ads), web fonts causing FOUT (Flash of Unstyled Text), and embeds (iframes, third-party widgets) without reserved space. The fixes: always set `width` and `height` on images, use `font-display: swap` with size-adjusted fallbacks, reserve space for dynamic content with `min-height`, and load third-party embeds in a fixed-size container.
Third-party scripts: the silent CWV killer
Third-party scripts are responsible for a disproportionate share of CWV failures. Audit every script on your page using the Coverage panel in DevTools and the Third-Party Web dataset. Load analytics scripts with `defer` or `async`, delay non-essential scripts until after the page is interactive using Partytown or custom lazy-loading, and regularly audit which scripts are still providing value versus just burning performance budget.
Monitoring in production
The only CWV scores that matter for ranking are the ones measured on real users in the Chrome User Experience Report (CrUX). Set up web-vitals.js to send real-user measurements to your analytics platform, create dashboards segmented by page type (home, PDP, article), device type (mobile vs desktop), and connection speed. Set alert thresholds 20% above the passing threshold — so you catch regressions before they reach the CrUX data that Google sees.
