Lab scores are a diagnostic. Field data is the truth. The gap between them is where most performance work goes to die, so here's what actually moved real-user numbers on a large content-managed site.
LCP is nearly always an image problem
On a content site the largest element is a hero image roughly nine times out of ten. Which means LCP is not really a JavaScript problem, and all the bundle-splitting in the world won't touch it.
What worked, in order of impact:
- Stop lazy-loading the hero. A
loading="lazy"on an above-the-fold image is a straight loss. Setpriorityon exactly one image per page and audit that it's the right one. - Preconnect to the image CDN. One line, and it removed about 150ms of connection setup from the critical path.
- Serve the right dimensions. A 2400px-wide hero on a phone is a common CMS-authoring outcome. Responsive
sizesfixed more than any compression tuning did. - Inline the LQIP. A tiny base64 placeholder makes the perceived load dramatically better even when the measured number is unchanged.
CLS is a reserved-space problem
Every layout shift traces back to something whose size wasn't known before it arrived. Images without dimensions, web fonts with different metrics, and — the worst offender on our site — a cookie banner injected by a tag manager.
Fonts are worth being specific about. font-display: swap trades an invisible-text period for a shift, and if your fallback has different metrics that shift is large. Setting size-adjust and matching ascent-override on the fallback face brought our font-driven CLS to essentially zero. Next's font module does most of this for you, which is reason enough to use it.
INP is where the real work is
INP replaced FID and it is a much harder test, because it measures every interaction rather than just the first. It also can't be faked with a better hosting plan.
The pattern that helped most was breaking up long tasks around user input. Anything above 50ms on the main thread during an interaction shows up. In practice that meant deferring non-urgent work with startTransition, and moving a genuinely expensive filter operation off the critical path.
Two things that weren't worth it
Micro-optimising the JavaScript bundle below a couple of hundred kilobytes made no measurable difference to any field metric. The time would have been better spent on images.
And chasing a perfect Lighthouse 100 actively hurt: we spent a week on synthetic score improvements that no real user experienced, while a genuine INP regression sat unnoticed in the field data. Instrument real users first, and let that decide what you work on.
