Next.js applications that feel slow in production usually don't have one dramatic bug — they have an accumulation of small architectural decisions that each shipped a bit more JavaScript to the client, or fetched data a bit less efficiently, than necessary.
Default to Server Components, opt into Client Components
The App Router's default is server rendering, and the most common performance mistake we see is fighting that default — marking components "use client" reflexively, often for a whole page, when only a small interactive piece within it actually needs to run in the browser. Every component that doesn't need client-side interactivity should stay a Server Component. This isn't a micro-optimization; it directly determines how much JavaScript ships to the browser.
The practical pattern: build pages as Server Components, and push "use client" down to the smallest possible leaf components — a button with an onClick handler, a form with local state — rather than up to the page level.
Data fetching location matters as much as the query itself
Fetching data in a Server Component, close to where it's rendered, avoids waterfalls where the client has to render, then request data, then re-render. Parallel data fetching — issuing independent requests concurrently rather than awaiting them one after another — is an easy win that's frequently left on the table simply because sequential await calls are the path of least resistance to write.
Don't skip the built-in image and font optimization
next/image and next/font solve real, measurable performance problems — layout shift, oversized images, render-blocking font loads — and skipping them "because it's simpler" tends to be a false economy. The API surface is small enough that the performance cost of not using them rarely buys you meaningful simplicity in return.
Caching and revalidation need to be a deliberate decision
Next.js's caching model is powerful and, if left to defaults without consideration, can produce confusing behavior — stale data appearing after a mutation, or unnecessary refetches. Decide deliberately, per route or fetch, what caching strategy makes sense: static generation for content that changes rarely, time-based revalidation for content that changes occasionally, and explicit on-demand revalidation triggered by mutations for anything that needs to be immediately fresh after a write.
Measure on real conditions, not a warm developer laptop
A page that feels fast in local development, on a fast machine with a warm cache and a fast connection, can be meaningfully slower for a real user on a mid-range phone over an average connection. Testing against throttled network and CPU conditions — and against production builds, not dev mode — catches problems that a smooth local development experience will hide from you.
None of this requires exotic techniques. It requires treating "where does this run, and when does this data get fetched" as a deliberate architectural decision on every page, rather than an accident of how the code happened to get written.