Measure the real thing before changing anything
A green Lighthouse score in a lab environment and a Core Web Vitals report from the Chrome UX Report disagreeing is one of the most common things I see, and it's not a contradiction — they're measuring different populations. Lighthouse runs once, on a fast machine, on a throttled but consistent connection. Field data (CrUX, or your own Web Vitals reporting) is every real visitor, on whatever device and network they actually have. If a client's field LCP is poor and their lab score is fine, the fix has to start with field data, or you'll optimise the wrong page.
LCP is almost always one of three things
- The largest element is an image or video that isn't preloaded and is discovered late — usually because it's set as a CSS background-image, or loaded by client-side JavaScript instead of being in the initial HTML.
- The server or edge is slow to respond — TTFB eating into the budget before the browser has even started painting.
- A render-blocking resource — an unoptimised font load, a synchronous script in the head — delays first paint of everything, including the LCP element.
In Next.js specifically, this usually means: use next/image with priority on the actual LCP element (and only that element — priority on everything defeats the purpose), make sure the hero content is server-rendered rather than fetched client-side, and check font-display and preloading for the typeface used above the fold.
INP is a JavaScript problem, almost without exception
Interaction to Next Paint replaced First Input Delay as a Core Web Vital because it measures the whole interaction, not just the first one. A slow INP means the main thread is busy when the user clicks — usually because too much JavaScript is shipped for the interactivity actually needed, or because a state update triggers an expensive re-render of something far larger than the part that actually changed.
The fix is rarely "add more memoisation everywhere." It's finding, via a profiler trace, the specific handler or render that's blocking the thread for the longest stretch, and either deferring the non-urgent part of it or reducing what re-renders. Two or three specific fixes based on a trace beat a general pass of React.memo scattered through the codebase.
CLS is a layout-reservation problem
Every CLS issue I've traced comes down to the same root cause: something is inserted into the page after layout without space reserved for it — an image without width/height (or a CSS aspect-ratio), a web font swapping in in and shifting text, an ad slot or a cookie banner that pushes content down after it loads. The fix is almost always reserving the space up front, not preventing the insertion.
What I deliberately don't do
I don't apply a fixed "performance checklist" to every project regardless of where its actual budget is going, and I don't promise a specific score before a baseline exists — anyone who does either hasn't looked at the trace yet. The honest version of this work is: measure, find the two or three things actually costing the time, fix those, remeasure, and put a budget in CI so the next feature doesn't quietly undo it.

