All insights
11 min readFirstSoft

Most Next.js performance problems are architecture problems wearing a performance costume. Here's how we design Next.js apps that stay fast — and stay up — when traffic spikes.

Next.jsPerformanceArchitectureEngineering

A site can score a perfect Lighthouse number on a quiet Tuesday and still fall over the moment it gets the attention it was built to attract. Performance under load is a different problem than performance in isolation, and most Next.js apps that crack under traffic do so for architectural reasons, not because a single component was slow. The good news is that the App Router gives you precise control over where work happens and when — and used deliberately, it lets you build apps that stay fast when it matters most.

Render at the right time, not just the right place

The most consequential decision in a Next.js app is when each piece of content is rendered. Get this right and most other performance concerns shrink. The App Router gives you a spectrum, and the skill is matching each route to the cheapest option that still serves correct data:

The rendering strategies, from cheapest to most dynamic:

  • Static generation (SSG). Render at build time and serve from the edge. For marketing pages, docs, and anything that doesn't change per request, this is effectively free under load — there is no per-request work to overwhelm. It is the default we reach for.
  • Incremental Static Regeneration (ISR). Serve static content but revalidate it on a schedule or on demand. You get static-level performance for content that changes occasionally — pricing, listings, published articles — without rebuilding the whole site.
  • Server-side rendering (SSR). Render per request on the server. Necessary for genuinely personalised or real-time pages, but it is the strategy that consumes server resources per visitor — so it is where traffic spikes turn into outages if applied indiscriminately.
  • Streaming with Suspense. Send the shell immediately and stream slower parts as they resolve. This keeps first paint fast even when part of the page depends on a slow data source, instead of blocking the whole response on the slowest query.
The cheapest request is the one your server never has to compute. Push everything you can to build time or the edge, and reserve per-request rendering for the work that genuinely needs it.

Server Components shrink the bundle, not just the work

React Server Components are often discussed as a server-rendering feature, but their quiet superpower is on the client: code that runs only on the server never ships to the browser. Data-fetching logic, heavy formatting libraries, markdown parsers, and the like stay server-side, so the JavaScript the user downloads and the browser has to parse shrinks. Less client JavaScript means faster interactivity, which matters most on mid-range mobile devices where parsing and execution — not the network — are the bottleneck. We keep components as Server Components by default and reach for the client only at the leaves of the tree where interactivity actually lives.

The database is where most apps actually fall over

When a Next.js app collapses under traffic, the front end is usually not the culprit — the database is. Three failure modes account for most incidents, and all three are designed-out rather than tuned-away:

  • Connection exhaustion. Serverless functions each open their own database connections, and a traffic spike can multiply those past the database's limit until everything stalls. A connection pooler sized for your concurrency is not optional at scale — it is the difference between graceful load and a cascading failure.
  • N+1 queries. A list that fires one query per row turns a single page view into hundreds of round trips. Under load, that multiplies into a self-inflicted denial of service. We fetch in sets and shape queries around how pages actually read data.
  • Missing indexes and unbounded queries. A query that scans the whole table is fine with a thousand rows and fatal with a million. Indexing the columns you filter and sort on, and never fetching unbounded result sets, keeps query time flat as data grows.

Cache deliberately, at every layer

Caching is the highest-leverage performance work available, because a cache hit replaces real computation with a near-instant lookup. Next.js gives you several layers, and a resilient app uses them on purpose rather than by accident:

The caching layers worth designing for:

  • The full-route cache and data cache, so rendered output and fetched data are reused instead of recomputed on every request.
  • Granular revalidation by tag or path, so you invalidate exactly what changed when content updates — keeping pages fresh without throwing away the whole cache.
  • A CDN at the edge, so static and cacheable responses are served close to the user and never reach your origin under load.
  • An application cache (such as Redis) for expensive computed results and session-scoped data that several requests share.

Design for failure, because dependencies fail

A high-performance app is also a resilient one — speed is worth little if a single slow dependency takes the whole page down. We set timeouts on external calls so a hanging third party can't hold a request hostage, add retries with backoff for transient errors, and use Suspense boundaries with sensible fallbacks so one failing widget degrades to a graceful placeholder instead of a blank screen. The goal is an app that bends under stress and recovers, rather than one that snaps.

Measure under realistic load, not on your laptop

Performance you haven't measured under load is a guess. A synthetic Lighthouse run on a fast laptop and a quiet server tells you almost nothing about behaviour at peak. We profile against realistic conditions — load testing to find where latency climbs and connections saturate, real-user monitoring to see Core Web Vitals from actual devices and networks, and tracing to locate the slow span inside a slow request. You optimise what you can see; the work is making sure you can see the right things.

How we build it

We design the rendering strategy, the data layer, and the caching plan together, from the first architecture conversation — because performance under traffic is decided by those structural choices long before any single component is written. The result is a Next.js app that is fast on a quiet day, stays fast on its busiest one, and degrades gracefully when something upstream misbehaves. That is what we mean by architecture that doesn't crack under traffic: not a clever optimisation bolted on at the end, but a system designed from the start to hold.

Work with us

From high-performance Next.js builds to SEO and GEO that compound, we architect and grow the platforms ambitious teams run on. Tell us what you're building.