Lighthouse runs on your laptop. Your users run on mid-range Android phones over hotel Wi-Fi. Field data is the only data that counts.

The three numbers

Metric Measures Good The usual culprit
LCP Time to largest visible element < 2.5s Unoptimized hero image
INP Worst interaction latency < 200ms Heavy hydration, long tasks
CLS Visual stability < 0.1 Unsized images, injected banners

LCP: it is almost always the hero image

The fix is nearly mechanical in Next.js:

components/hero.tsx
import Image from "next/image";
 
<Image
  src="/hero.png"
  priority
  width={1200}
  height={630}
  alt="Product dashboard"
/>

priority disables lazy loading and preloads the image. Use it on exactly one above-the-fold image per page — marking everything priority is the same as marking nothing.

Check the preload chain: HTML → CSS → image discovery is a waterfall. priority flattens it by preloading from the initial HTML response.

INP: stop shipping JavaScript to fix JavaScript

INP regressions are hydration debt. The main thread is busy executing your bundle when the user taps.

  • Move non-interactive components back to the server
  • Split long tasks — anything over 50ms blocks input
  • Defer third-party scripts with next/script and strategy="lazyOnload"
  • Measure with the Long Animation Frames API, not intuition

CLS: reserve the space

Layout shift is a promise-keeping problem. Everything that appears later must have its space reserved now:

.ad-slot {
  min-height: 250px; /* reserved before the ad loads */
}

next/image handles images. next/font handles font swap shift. What remains is your own late-loading UI — banners, embeds, cookie bars.

The loop that works

  1. Read field data (CrUX, or your own RUM)
  2. Find the worst URL group, not the average
  3. Fix one thing
  4. Wait 28 days for the field window to roll
  5. Repeat

Performance work is compounding. A fast site is not a project — it is a habit with a dashboard.