From routing decisions to RSC adoption, incremental builds to edge functions — the non-obvious architectural choices we've made and why they held up.
We've shipped Next.js applications across a wide range: a B2B SaaS with 50,000 active users, a consumer marketplace handling $2M monthly GMV, internal tooling for teams from 5 to 500, and a real-time data dashboard processing 10,000 events per minute. Each one taught us something the documentation doesn't.
The App Router migration: when to do it and when not to
We migrated three production apps to the App Router in 2023. Two went smoothly. One was painful enough that we'd make a different call in retrospect. The variable that determined which was which: the complexity of the data fetching layer.
Apps with simple, co-located data fetching migrated cleanly — the new model (fetch in Server Components, pass down to Client Components) simplified our code. Apps with complex, cross-cutting data requirements that were managed via Context and global state stores required significant architectural rework. If your app is heavily context-dependent, budget twice the migration time you think you need.
React Server Components: the right mental model
The RSC mental model that helped us most: think of Server Components as the database-access layer of your frontend. They fetch, they transform, they render to HTML. They don't handle events, they don't maintain state, they don't run in the browser. Once you stop thinking of them as 'components that happen to run on the server' and start thinking of them as 'server-side rendering with proper composability,' the right patterns become obvious.
The boundary decision — where to place 'use client' — is the key architectural choice. Our rule: push client boundaries as far down the tree as possible. A page that has one interactive dropdown doesn't need to be a Client Component — just the dropdown does.
Edge functions: not for everything
Edge functions are excellent for: auth middleware, A/B testing, geolocation-based routing, and lightweight response transformations. They are not excellent for: database access, heavy computation, or anything that requires Node.js APIs. The cold start improvement is real, but so are the constraints.
We've settled on a pattern: edge functions handle auth and routing logic, Node.js runtime handles everything that needs a database connection. Don't try to run Prisma at the edge. We learned this the expensive way.
Build performance at scale
- Incremental Static Regeneration (ISR) for high-traffic, infrequently-updated pages — not everything needs to be dynamic
- Partial Prerendering (PPR) where stable shells surround dynamic content — the best of both worlds for content-heavy apps
- Bundle analysis on every deployment — we've caught 3x bundle size regressions from carelessly imported libraries
- Image optimization through next/image is non-negotiable — the LCP improvements are significant and automatic
- Turbopack for local development — it's production-ready now and the speed difference is significant
The caching model deserves dedicated study
Next.js 14+ has four caching layers: the Request Memoization cache, the Data Cache, the Full Route Cache, and the Router Cache. Most developers understand one of these. Understanding all four — and how they interact — is what separates Next.js apps that are fast from ones that are mysteriously slow or serve stale data.
Spend half a day reading the Next.js caching documentation in full. Not skimming — reading. It will save you weeks of debugging inexplicable stale-data bugs and cache invalidation issues. This is one of those investments that compounds.
What we'd tell ourselves three years ago
Don't optimise routing structure for the framework's features — optimise for your team's mental model. The 'perfect' file structure according to the docs is not the one your team will maintain most effectively. Start simple, refactor toward the framework's model as you hit actual constraints. And always measure before optimising — Next.js is fast by default. Most performance problems are data-fetching problems masquerading as rendering problems.
