Home/Insights/Engineering

Next.js at scale: lessons from 3 years of production apps

SS
Sylvester SFounder & CEO
Jan 30, 2025·10 min read
Code on a monitor

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.

Frequently asked questions

When should I migrate from the Pages Router to the App Router in Next.js?

When the migration cost is lower than the ongoing cost of working around the Pages Router's limitations for your specific use case. If your data fetching is simple and co-located, migration is usually straightforward. If your app relies heavily on global state, Context providers, or complex cross-cutting data requirements, budget significantly more time and consider a gradual migration rather than a full cutover.

What is the simplest way to think about React Server Components?

Think of them as the database-access layer of your frontend. They fetch data, transform it, and render to HTML on the server. They do not run in the browser, do not handle events, and do not maintain state. The key decision is where to draw the boundary with Client Components. Push that boundary as far down the component tree as possible.

Are edge functions worth using in a Next.js app?

For auth middleware, geolocation routing, and lightweight response transformations, yes. For anything that needs a database connection, heavy computation, or Node.js-specific APIs, no. Our rule is: edge functions handle routing and auth logic, the Node.js runtime handles everything that needs to talk to a database. Do not try to run Prisma at the edge.

Why does my Next.js app sometimes serve stale data even after I update the content?

Next.js has four distinct caching layers, and they interact in ways that are easy to get wrong. The most common cause of unexpected stale data is a mismatch between revalidation settings at the route level and the fetch level. Spend time reading the caching documentation fully, not skimming it. Understanding all four layers is what separates Next.js apps that behave predictably from ones with mysterious stale-data bugs.

Start a project

More in Engineering

Database and data visualization
Engineering · Dec 5, 2024

Why we stopped using ORMs for complex queries