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:
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.
priorityflattens 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/scriptandstrategy="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
- Read field data (CrUX, or your own RUM)
- Find the worst URL group, not the average
- Fix one thing
- Wait 28 days for the field window to roll
- Repeat
Performance work is compounding. A fast site is not a project — it is a habit with a dashboard.
