Fix Core Web Vitals Without a Rewrite
15 Jan 2026Core Web Vitals feel like a grading system designed by someone who’s never shipped a deadline. You don’t need to rewrite your app to move the numbers. Most sites have one or two specific mistakes dragging each metric down, and each has a targeted fix.
LCP: find the actual element first
Largest Contentful Paint is almost never “the whole page is slow” — it’s one specific element (usually a hero image, sometimes a big text block behind a slow font) that renders late. Find it before you optimize anything:
Open Chrome DevTools → Performance panel → record a load → look for the “LCP” marker in the timeline. It names the exact element.
Common causes and fixes:
| Cause | Fix |
|---|---|
| Hero image loads after JS bundle | Use a plain <img> or next/image with priority, not a component that mounts after hydration |
| Image isn’t preloaded | Add <link rel="preload" as="image" href="..."> for the specific LCP image |
| Render-blocking CSS/JS | Defer non-critical CSS, inline critical CSS for above-the-fold content |
| Slow server response (TTFB eats into the LCP budget) | Cache the route, or check for a slow DB query in the initial render path |
The priority prop matters specifically for next/image — without it, the image lazy-loads by default, which is exactly wrong for the one image that determines your LCP score:
<Image src="/hero.jpg" alt="" width={1200} height={600} priority />
CLS: reserve the space before content arrives
Cumulative Layout Shift is caused by things that change size after they’ve already rendered, pushing everything below them down. The fix is almost always the same shape: reserve the space up front.
- Images without dimensions: always set
width/height(oraspect-ratioin CSS) so the browser reserves space before the image loads. - Ads and embeds: wrap in a container with a fixed min-height matching the expected content size.
- Web fonts swapping: a FOUT/FOIT swap that changes line height causes shift — set
font-display: optionalor size your fallback font to closely match the real one usingsize-adjust. - Content injected above existing content: banners, cookie notices, or “you have new messages” bars that push the page down after load. Reserve their space or make them overlay instead of pushing.
img {
aspect-ratio: attr(width) / attr(height);
}
INP: profile the actual interaction, not the whole page
Interaction to Next Paint replaced FID in 2024, and it measures the full cost of an interaction — not just when the event handler starts, but until the next paint after it finishes. Slow INP usually means the handler itself is doing too much work synchronously.
- Heavy state updates on every keystroke: debounce or move expensive work (filtering a large list, re-rendering a big table) off the main thread of every render.
- Large re-renders on click: profile with React DevTools Profiler to find components re-rendering when they don’t need to — memoize or split state so a click in one area doesn’t re-render the whole tree.
- Long tasks blocking input: break up any synchronous loop over more than a few hundred items; use
requestIdleCallbackor chunk the work.
// Bad: filters 10,000 items synchronously on every keystroke
onChange={(e) => setResults(bigList.filter(x => x.includes(e.target.value)))}
// Better: debounce the expensive work
const debouncedFilter = useMemo(() => debounce(filterFn, 150), [])
Measure with real data, not just Lighthouse
Lighthouse runs in a lab, on one machine, once. Your actual Web Vitals score is judged on real user data (CrUX, or your own RUM). A page that scores well in Lighthouse can still fail in the field if your real users are on slower connections or older phones than your test machine. If you can, wire up the web-vitals library to send real metrics to your analytics:
import { onLCP, onCLS, onINP } from 'web-vitals'
onLCP(console.log)
onCLS(console.log)
onINP(console.log)
The takeaway
Don’t optimize blind. Identify the specific LCP element, the specific layout-shifting element, and the specific slow interaction — then fix that one thing. Three targeted fixes usually move all three scores more than a broad “make everything faster” pass.