Start with the exact symptom
When someone says that a Next.js site is slow, I do not change the first component I see. I write down which route is slow, for whom, in which environment and whether the problem is build time, server response, download size or interaction.
That distinction saves time. A five-second local build and a five-second browser load are different problems. They can share a dependency, but they need different measurements and different fixes.
- Record the failing route
- Separate build, server and browser symptoms
- Write down what changed before the regression
Read the build output
The production build is a useful diagnostic report. I look for routes that became dynamic unexpectedly, large client bundles, image warnings and dependency messages. I compare the current output with a known good run instead of treating every warning as the cause.
Node and package versions matter too. A project can compile on one machine and fail in CI because the runtime, lockfile or native package differs. Before editing application code, I confirm that the environment matches the project requirements.
- Run the production build locally
- Check the Node.js version
- Compare route and bundle output
Trace the server and client boundary
A common Next.js performance mistake is turning a large page into a client component because one small interaction needs state. That can move data fetching and rendering work into the browser. I keep the page and data on the server when possible, then isolate the interactive piece.
I also look at repeated requests and serialization. Fetching the same data in several components or sending a large object through a client boundary can be more expensive than the visible UI suggests.
- Keep client components narrow
- Avoid duplicate data fetching
- Send only the data an interaction needs
Fix the largest measured cost first
Once the bottleneck is clear, I make one focused change. It may be an image size, a client boundary, a query, a dependency or a cache setting. I avoid combining five optimizations because the result becomes impossible to explain and harder to roll back.
The fix should improve a user-visible metric or remove a concrete build problem. Cleaner code is valuable, but it is not proof that the original issue was solved.
- Change one expensive thing at a time
- Prefer removing work over hiding it
- Keep the patch easy to revert
Prove and document the fix
I rerun the command that first exposed the issue, then repeat the browser or route measurement. If the result is better, I record why. If it is not, I undo the assumption and test the next boundary.
A durable fix leaves a guardrail: a test, a build check, a bundle budget, a note in the runbook or simply a clearer ownership boundary. Debugging is finished when the same failure is less likely to return, not when the terminal turns green once.