Next.js has matured into the default choice for production React apps, but the framework gives you a lot of rope. Knowing which rendering mode to reach for - and when to stop reaching - is what separates a snappy site from a bundle-bloated one.
Picking the Right Rendering Mode
Every route is a decision. The App Router defaults to server components and static rendering, which is usually what you want - but the moment you reach for cookies(), headers(), or a dynamic segment without generateStaticParams, you've opted into SSR whether you meant to or not.
- Static (SSG) for marketing pages, docs, and anything derived from content. Cheap, fast, cacheable at the edge.
- Dynamic (SSR) for authenticated dashboards and personalized views. Pair with
unstable_cacheorrevalidateTagso you aren't round-tripping the database on every request. - Client components only when you need state, effects, or browser APIs. Keep them at the leaves of the tree.
Keeping the Bundle Honest
The easiest performance win is shipping less JavaScript. A few habits that pay for themselves:
- Run
@next/bundle-analyzerin CI and fail the build on regressions over a threshold. - Prefer
next/dynamicwithssr: falsefor heavy client-only widgets (charts, editors, video players). - Audit your
"use client"boundaries - a single misplaced directive can drag half your tree to the client.
Data Fetching That Scales
Colocate fetches with the components that need them, cache aggressively with tags, and invalidate with revalidateTag rather than blanket revalidatePath calls. For mutations, Server Actions keep request plumbing out of your components while TypeScript still catches shape mismatches end-to-end.
The boring parts - measuring, caching, and saying no to a fifth heading font - are what make a Next.js app feel fast a year after launch.