Next.js is fast by default. It is also very easy to make slow by accident.
Most performance problems I see in Next apps are not about React being slow or Vercel being expensive. They come from unclear boundaries, duplicated work, and letting “temporary” decisions live forever.
These are notes on how to keep a Next.js codebase fast to build, predictable to run, and pleasant to work in.
Start by making the codebase legible
Before touching performance tools, make the project understandable.
If a new developer cannot tell where hooks live, where API logic lives, or where page copy lives within a few minutes, you will pay for that confusion forever.
A boring structure wins.
Keep one place for hooks. One place for shared UI components. One place for content and metadata. Thin pages that mostly wire things together.
app/
dashboard/
solutions/
foo/
bar/
components/
hooks/
lib/
This does not make the app faster at runtime. It makes humans faster. That compounds.
DRY is about removing decisions, not saving lines
Duplicated logic forces the reader to make choices.
Which metadata file is correct. Which SEO description was updated. Which API client variation should I use.
Centralize anything that changes together.
export function buildMetadata(input: MetadataInput): Metadata {
return {
title: input.title,
description: input.description,
openGraph: { ... },
twitter: { ... },
};
}
When every page uses the same builder, you stop worrying about consistency. You trust the system again.
Trust is performance.
Prefer server components until proven otherwise
Client components are easy. They are also expensive.
Every client component ships JavaScript, runs hydration, and increases build and bundle cost. Use them only when you actually need interactivity.
If a component does not use state, effects, or browser APIs, keep it on the server.
export default async function Page() {
const data = await getData();
return ;
}
No use client. No hydration. Less work.
You can always move logic client side later. Moving it back is harder.
Cache aggressively, but intentionally
Next gives you caching for free, but only if you let it.
Use fetch with caching semantics instead of rolling your own.
await fetch(url, { next: { revalidate: 60 } });
Avoid wrapping everything in dynamic just because something broke once. That turns your app into an expensive SPA with extra steps.
If something must be dynamic, isolate it. Do not poison the whole page.
Keep API clients dumb
Your API client should not know about routing, auth state, or UI behavior. It should send requests and return data.
Once we added a server proxy, the client became simpler instead of more complex.
await fetch('/api/backend/payments', {
credentials: 'include',
});
No tokens. No headers. No branching logic.
The server handles auth. The client just asks for data.
This improves security and reduces mental load.
Never store tokens in localStorage
It is convenient. It is also unsafe.
LocalStorage is readable by any injected script. There is no mitigation once it is compromised.
If you care about security, use httpOnly cookies and move auth to the server.
cookies().set('access_token', token, {
httpOnly: true,
secure: true,
});
Yes, it requires more setup. Yes, it is worth it.
Reduce build work by deleting code
Most build optimizations come from removing work, not adding tools.
Things that slow builds quietly: Duplicated imports Large client components Overused dynamic rendering Unused dependencies Copy pasted config
Before reaching for caching layers, ask what the compiler is doing repeatedly that it should not be doing at all.
Treat DX issues as real bugs
If a fresh clone does not boot cleanly, that is a bug. If formatting changes every commit, that is a bug. If Node versions drift, that is a bug.
Pin your environment.
# .nvmrc
20
Commit generated files that tools expect. Pick one package manager. Make the happy path obvious.
The goal is that nothing surprising happens on day one.
Make failures explicit
If the backend is down, fail clearly. If an image format is unsafe, opt into it intentionally. If something is deprecated, address it early.
Silent failures turn into folklore. Loud failures turn into fixes.
The real optimization
A fast Next.js app is not one with the most tricks. It is one that does the least unnecessary work. For the browser. For the server. For the developer.
When the structure is clear, the build gets faster. When the security model is clean, the code gets simpler. When the project explains itself, people stop being afraid to change it.
That is when Next.js starts to feel boring again.
And boring is exactly what you want.