Fixing Core Web Vitals in a Next.js website
A practical workflow for finding the real cause of slow loading, delayed interactions and layout shifts—then fixing the problem without guessing.

Measure before changing anything
Performance work becomes expensive when it starts with a list of fashionable fixes. Converting every image to a newer format or adding memoization everywhere may change a score, but it does not tell us what users are waiting for. I begin by writing down the slow page, the device and network conditions, and the exact symptom. Is the main content late, does a click feel ignored, or does the page move after it appears?
I use field data when it is available because it represents real devices and real connections. Lab tools are still valuable: Lighthouse gives a repeatable starting point, the Performance panel explains the main thread, and a production build reveals behavior that development mode can hide. One test is never treated as proof. I repeat the same scenario and compare the median rather than celebrating the best run.
- LCP asks when the largest useful content becomes visible
- INP asks how quickly the page responds to interactions
- CLS asks whether visible content moves unexpectedly
- Record the test route, viewport, network and build mode
Find the LCP element, then follow its request
A poor Largest Contentful Paint score is not automatically an image problem. The LCP element may be a heading waiting for a web font, a hero image discovered late through CSS, or server-rendered content delayed by data fetching. DevTools identifies the element; the network waterfall then shows when its request began and what blocked it.
For a Next.js hero image, I give the browser dimensions through next/image, use a correct sizes value, and mark only the genuinely above-the-fold asset as priority. I avoid serving a desktop-sized file to a narrow phone. If text is the LCP element, I inspect font loading and reduce unnecessary weights. If the initial HTML arrives late, image compression cannot solve the actual bottleneck; the server and data path need attention.
- Do not preload every image—competing high-priority requests can make LCP worse
- Keep the hero asset discoverable in the initial render
- Use responsive sizes that match the real layout width
- Remove render-blocking work before adding loading tricks
Make interactions respond immediately
Interaction to Next Paint is often affected by JavaScript that occupies the main thread when a person clicks, types or opens a menu. I record a slow interaction and inspect the long task around it. Large client components, expensive filtering, synchronous third-party scripts and repeated rendering are common causes, but the trace should decide which one matters on that page.
The first goal is feedback, not clever optimization. A button should acknowledge the action quickly even if the larger task continues. I keep state close to the component that needs it, split heavy client code at sensible boundaries, and move work off the interaction path where possible. Memoization is useful only after a profiler shows repeated expensive work; otherwise it adds another layer to reason about.
Reserve space before content arrives
Cumulative Layout Shift is usually a layout-contract problem. The browser renders what it knows, then moves content when an image, font, banner or embedded widget finally reveals its size. The most reliable fix is to reserve the final space from the beginning.
Images need intrinsic dimensions or a stable aspect-ratio container. Loading skeletons should resemble the final component rather than being shorter placeholders. Notices inserted above existing content should either be present from the first render or use an overlay that does not push the page. Font fallback can also change line breaks, so I keep the font set small and use the framework's font tooling consistently.
- Set width and height or use a fixed aspect ratio for media
- Do not inject late content above what the user is reading
- Give dynamic widgets a realistic minimum height
- Check mobile wrapping because it can expose shifts hidden on desktop
Send less JavaScript to the browser
With the App Router, a page does not need to become a client component just because one button is interactive. I keep content, metadata and data mapping on the server, then isolate the smallest interactive part behind a client boundary. This reduces hydration work and makes the initial page useful sooner.
Bundle analysis is most helpful when it answers a concrete question: why is this route shipping code it never uses? I check large libraries, duplicated utilities and components imported too high in the tree. Removing a dependency is not always necessary; sometimes a direct import or route-level dynamic import is enough. The important part is that the loading behavior still feels stable and accessible.
Verify the fix without inventing a success story
After changing one bottleneck, I rerun the original test under the same conditions. I compare the trace, not only the headline score, and check that the change did not damage image quality, keyboard behavior or loading states. A faster page that becomes confusing is not a successful optimization.
Lab improvements are evidence that the direction is promising, not proof that every visitor is now fast. Field data needs time and enough traffic to update. Until that evidence exists, I describe the work honestly: what was observed, what changed, and what the controlled test showed. That is more useful than attaching an impressive percentage to a result that cannot be verified.
- Retest the same page and conditions
- Check visual quality and accessibility after optimization
- Monitor real-user data when the product has sufficient traffic
- Document the decision so the regression is less likely to return
