Every year someone declares Core Web Vitals "solved." Every year, the median website is still measurably slower than it was the year before. The problem isn't that the metrics are mysterious. They're well-documented. The problem is that performance work feels abstract until your bounce rate climbs or a stakeholder forwards a Lighthouse report with red across the board.

This is the playbook we wish someone had handed us when we started caring about this seriously. It's organized by metric, with the fixes that have actually moved real numbers on real sites.

What Core Web Vitals actually measure

Three metrics define modern web performance:

  • Largest Contentful Paint (LCP) — how quickly the main content of the page becomes visible. Goal: under 2.5 seconds.
  • Interaction to Next Paint (INP) — how snappy the page feels when a user clicks, taps, or types. Goal: under 200ms.
  • Cumulative Layout Shift (CLS) — how much the page jumps around as it loads. Goal: under 0.1.

Two things matter about these numbers:

  1. They're measured at the 75th percentile of real user visits, not your fast laptop on Wi-Fi.
  2. Lighthouse is a useful approximation, but field data (Chrome User Experience Report) is the ground truth Google actually uses for ranking signals.

If you only fix what's broken in your local Lighthouse run, you'll miss what's broken for the user on a three-year-old Android phone, which is most of them.

Fixing LCP

LCP is almost always one of three things: a hero image, a hero heading, or the first big paragraph of content. Find which one with the Performance panel in Chrome DevTools — there's a literal "LCP" marker.

Then, in order of impact:

Serve the LCP image well

If your LCP is an image, the win comes from three things working together:

  • Format: AVIF or WebP, with a JPEG fallback. AVIF cuts file size by 30–50% over JPEG without visible quality loss.
  • Dimensions: serve the actual size the browser will display, with a srcset so the right one gets picked. Stop sending 4000px hero images to a 390px phone screen.
  • Priority: <img fetchpriority="high"> for the hero, loading="lazy" for everything below the fold.

In Next.js, the <Image> component does most of this for you, but only if you actually use it. Many slow Next.js sites have one offending <img> tag in a header somewhere.

Get the HTML out the door faster

Your LCP can't paint until the HTML arrives. Common culprits:

  • A slow API call blocking server-side rendering. Cache aggressively or move it to client-side after the first paint.
  • A bloated middleware that runs synchronous work on every request.
  • Cold starts on serverless functions in regions far from your users.

Time-to-first-byte (TTFB) is the foundation everything else sits on. If TTFB is 800ms, no amount of frontend optimization saves you.

Stop blocking with fonts

Custom fonts can delay LCP by hundreds of milliseconds if you let them. Use font-display: swap, preload only the weights you actually need, and self-host them. A single self-hosted variable font is faster than three external font files.

Fixing INP

INP replaced FID a couple of years ago and is dramatically harder to game. It measures every interaction across the page's lifetime, not just the first one.

Break up long tasks

Most INP regressions come from a single function blocking the main thread for too long. The browser can't respond to clicks while JavaScript is running.

Practical fixes:

  • Use useDeferredValue and useTransition in React 18+ for expensive renders.
  • Move non-urgent work off the main thread — Web Workers are still underused.
  • Yield to the browser inside long loops with await new Promise(r => setTimeout(r, 0)) or the scheduler.yield() API.

Audit your event handlers

Look at every onClick, onChange, and onScroll in critical UI. Anything doing meaningful synchronous work is a candidate for INP regression. Specifically:

  • Synchronous JSON parsing of large blobs
  • Recomputing derived state across thousands of items
  • Dispatching to a global store that re-renders the world

Be careful with hydration

If your framework is hydrating a huge component tree on the first user interaction, INP suffers. React Server Components, partial hydration, and "islands" architectures exist precisely to fix this.

Fixing CLS

CLS is the easiest of the three to fix, because the causes are short and well-known.

  • Images and videos without dimensions — set width and height (or aspect-ratio in CSS) so the browser reserves space.
  • Ads and embeds that load asynchronously into a 0px container, then push everything down.
  • Web fonts swapping in with different metrics. Use size-adjust or CSS @font-face descriptors to match fallback metrics.
  • Banners injected at the top of the page after first paint. Reserve their height up front, even if it's empty.

Audit your site by scrolling slowly through it on a throttled connection. Anything that visibly jumps is a problem.

A monitoring loop that works

Optimization without measurement is theater. The minimum viable monitoring setup:

  1. Real User Monitoring (RUM)web-vitals npm package shipping to your analytics. Track LCP, INP, and CLS at the 75th percentile, broken out by page.
  2. Lab data on every PR — a Lighthouse CI run on key pages, failing the build if scores regress meaningfully.
  3. A budget — write down what's acceptable (e.g. "LCP under 2.5s on the homepage at p75") and treat regressions as bugs.

Without these three, every performance fix is a one-time hero project that quietly degrades a quarter later.

What doesn't actually help

A few things people spend time on that rarely move the needle:

  • Obsessively shaving 5KB off a 200KB bundle while a 600KB image dominates LCP.
  • Replacing your CSS framework "for performance" without measuring.
  • Removing third-party scripts one at a time without checking which ones are actually expensive.

A sane order of operations

If you only have a day, do these in order:

  1. Profile a real page in DevTools and find your actual LCP element.
  2. Optimize that element first.
  3. Set widths and heights on every image in the critical viewport.
  4. Audit your three biggest third-party scripts.
  5. Add RUM and a Lighthouse CI check.

You'll be done by lunch and your numbers will look noticeably better by Monday. That's usually enough to convince the rest of the team that performance is worth investing in — which is the actual unlock.