Next.js 16.3 benchmarked on two production apps

Piotr Żarów

CEO

2026-09-22

#Frontend

Time to read

14 mins

In this article

Introduction

Everything we measured, in one table

What we tested it on

Method, and why it changed our conclusions

Once the cache lands, type checking is the build

Share this article

Introduction

Part 1 of 2. Part 2 covers what the Cache Components migration actually costs

Next.js 16.3 promises less dev server memory, faster builds, faster server rendering, and a new opt-in suite called Instant Navigations. Vercel published their own numbers: up to 90% less RAM in dev, up to 5.5x faster builds on CI, up to 22% more requests under load.

We upgraded two projects we maintain, measured them before and after, and then, later than we should have, read the documentation.

Two projects, because one was not enough. The first is a small static marketing site, and it turned out to be too small to test half of what 16.3 claims. So we ran the same harness against a Medusa e-commerce storefront: 223 source files, a cart, a checkout, and almost every route server-rendered per request. Where the two agree, we believe the number.

The documentation is what changed our conclusions. The headline build improvement is real and large, and on the first project's deployment pipeline it would have delivered zero, because the pipeline throws away the one directory the feature depends on. Nobody would have noticed either: the build would have stayed the same speed and the upgrade would have looked like a dud.

This part covers the numbers: builds, type checking, dev memory, and throughput. Part 2 covers Instant Navigations, which is a migration rather than a benchmark.

Everything we measured, in one table

Two production codebases, every number interleaved A/B on the same machine, in September 2026 against Next.js 16.3.4:

The build cache is the one result that held across both codebases. Server rendering throughput ranged from 7% to 52% depending entirely on how much the app renders per request, so a single headline number for it is meaningless. Dev memory we could not measure at either size. And on a container pipeline the build cache delivers nothing at all until you carry .next/cache into the build yourself.

What we tested it on

A bilingual (Polish and English) B2B marketing site we build and maintain. Roughly 20 routes plus an API route, around 50 TypeScript and TSX source files, React 19, Turbopack, React Compiler enabled, deployed as a standalone build in Docker.

One characteristic dominates everything below: the site is almost entirely static. 22 of its 23 build outputs are prerendered at build time. There is no cookies(), no headers(), no searchParams, and no server-side fetch anywhere in the source. The only data request in the whole app is client-side, from a contact form.

We went from Next 16.1.6 to 16.3.4 on Node 24, with Yarn 4. The upgrade itself needed zero code changes.

The second project is a Medusa v2 e-commerce storefront: 223 TypeScript source files, 4.5x the first, with a cart, checkout, customer accounts and Stripe. It reads cookies() for cart id, JWT and cache id, and almost every route is ƒ (Dynamic), rendered per request. It is the opposite profile in every way that matters here, and it started two versions back on Next 15.3.9, which let us measure the real 15 to 16 migration as well.

Its build and dev numbers were taken with the commerce backend switched off, which is fine because those measure bundling 223 files. For the load test and the Cache Components work in part 2 we started the backend locally and rebuilt against real product data, because neither means anything against dead upstreams.

Method, and why it changed our conclusions

Our first pass was the obvious one: measure everything on 16.1.6, upgrade, measure again. That pass told us dev memory improved 5%, cold builds got 27% slower, and throughput improved 8%.

Two of those three findings were wrong.

Build times on this project are measured in seconds, and a laptop is a noisy place to measure seconds. So we re-ran everything as an interleaved A/B: 16.1.6, then 16.3.4, then 16.1.6 again, then 16.3.4 again, alternating in one session, with a discarded warm-up build at the start of each round. Where possible we used the compile time Next reports itself rather than wall clock, because it excludes yarn startup and type checking.

Under that method the "27% slower cold builds" vanished completely, and so did the memory improvement. Only the throughput gain survived.

If you are benchmarking your own upgrade, interleave your runs. A single before-then-after pass on a laptop will confidently tell you things that are not true.

Build times: cold unchanged, warm transformed

Cold builds, deleting .next before each run, nine runs per version:

Unchanged. The ranges overlap almost entirely.

Warm builds, no source changes, .next kept, seven runs per version:

On 16.1.6 there is effectively no build cache: rebuilding with zero changes costs the same 2.9 seconds of compilation as a cold build. On 16.3.4 the same rebuild compiles in about 0.19 seconds, roughly 15 times faster, and the whole next build drops 45%.

The Turbopack FileSystem cache is a build cache that Turbopack writes to .next/cache and restores on the next run, so unchanged modules are not compiled again. It became enabled by default for next build in Next.js 16.3, and you do not have to turn it on.

The same test on the bigger project, and what it separates

The storefront started on 15.3.9, so the jump to 16.3.4 crosses a major version. That conflates two different changes, and measuring 16.1.6 in the middle pulls them apart. Three cold runs and two warm runs per version, medians:

Two things fall out of that middle column.

The cold build win belongs to the bundler. Cold compile drops 12.0 s to 4.4 s, 2.7x, and every bit of it lands between 15.3.9 and 16.1.6, where Next 16 made Turbopack the default build bundler in place of webpack. From 16.1.6 to 16.3.4 the cold build does not move, which is exactly what we saw on the marketing site.

16.1 was a warm-build regression, and 16.3 is the fix. On webpack a warm rebuild compiled in 3.0 s. On Turbopack in 16.1.6 the same rebuild took 4.4 s, slower than the webpack it replaced, because Turbopack had no build cache yet. 16.3 takes it to 0.32 s. A team that upgraded to 16.1 and judged Turbopack on repeat builds would have concluded it was a downgrade, and at the time they would have been right.

Where the two projects agree

The directly comparable number is the 16.1.6 to 16.3.4 warm compile, measured with the same harness on both:

Two codebases with nothing in common in size or rendering model, and the build cache lands within about 10% of the same ratio on both. Of everything we measured, this is the result we would bet on holding for your project too, provided your pipeline keeps the cache around.

The part the upgrade guide buries

The docs on Turbopack FileSystem caching are blunt about it:

The build cache lives in .next/cache. Builds only get faster when that directory is restored before each build. Containerized builds start from a clean layer and do not carry .next/cache over unless you cache or mount it explicitly.

On our project .next/cache is 52 MB. We deleted just that one directory and rebuilt:

The compile step goes straight back to being 17x slower. The whole headline feature works only if that directory survives between builds.

Then we looked at our own pipeline:

  • The Dockerfile has a clean builder stage that does COPY . . followed by RUN yarn build. Every image build starts with no .next/cache.
  • The CI config had no cache: block at all.

So every production build on this project is a cold build. We would have upgraded Next, watched our CI times not move, and reasonably concluded that Vercel's build numbers were marketing, when in fact we were discarding the cache on every run.

We fixed it and checked the fix in the pipeline itself rather than only on a laptop. Our CI builds inside Docker, so a CI-level cache directory would not have helped: the build does not run in the job's workspace. Instead the Dockerfile now restores .next/cache from a small cache image, and exposes a stage that exports the warmed cache so CI can publish it for the next pipeline.

With that in place we changed a source file, so the build genuinely had to re-run, and built the image both ways:

Roughly 10x, inside the actual container build. The exported cache image is 53 MB, and re-exporting it after a build takes about 3 seconds because it reuses the builder layers.

If you build outside Docker, this is simpler: on GitLab CI, GitHub Actions, CircleCI and the rest it is just a cache entry for .next/cache, and Next's own CI build caching guide has a snippet per provider. If your pipeline builds images, as ours does, you need to carry the directory into the build yourself.

And if your pipeline genuinely cannot persist it, the docs suggest setting turbopackFileSystemCacheForBuild: false so the build stops writing 52 MB of cache that nothing will ever read.

Put this on the upgrade ticket. Bumping the version is the easy half; if nobody wires up cache persistence, the largest improvement in 16.3 never reaches your team.

Once the cache lands, type checking is the build

Compilation drops to about 0.19 seconds and the whole build still takes 3.5 seconds, so compilation stops being the bottleneck. We looked at what was left. TypeScript was costing 747 to 870 ms of every build, which made it the biggest measurable step.

16.3 anticipates this. next build now runs your project-local tsc CLI by default, specifically so it can use TypeScript 7, the native port Microsoft released with roughly a 10x speedup. TypeScript 7.0.2 is out and is latest on npm. Our project, like most, was pinned to ^5.

We measured tsc --noEmit against the project's own tsconfig.json, interleaved:

About 7x faster, with zero new type errors. Both runs exited clean.

Inside next build that is roughly 0.7 seconds off every single build, about 18% of our warm build time, stacked on top of what the Turbopack cache already gave us.

Two caveats, because we could not ship it on this project.

First, Yarn 4.12 cannot install TypeScript 7. Yarn applies a builtin compatibility patch to the typescript package, and that patch expects a file layout TS 7 no longer has:

1
2
Error: typescript@patch:...#optional!builtin<compat/typescript>:
ENOENT: no such file or directory, lstat '.../typescript/lib/_tsc.js'

Forcing a plain resolution through resolutions did not help, Yarn re-applies the patch. Second, typescript-eslint (pulled in by eslint-config-next) still declares a peer range of >=4.8.4 <6.0.0.

So the 7x is real and measured, but on a Yarn 4 stack it is currently blocked by the toolchain rather than by Next. On npm or pnpm you should hit only the eslint peer warning. Worth checking before you promise it to anyone.

Dev server memory: we could not measure it, and now we know why

We cleared .next, started the dev server, requested all 21 routes so every one compiled, then summed RSS across the entire next dev process tree. Five samples per version.

The spread inside a single version is about 800 MB, which swamps any difference between them, so we measured nothing. The docs explain why, and the explanation is worth more than the non-result.

The 90% figure comes from two features: disk caching for dev, and memory eviction. Disk caching for dev shipped in 16.1. Our baseline was 16.1.6, so it already had half the improvement. Only eviction was genuinely new in our comparison.

So we tested eviction directly, on 16.3.4, over a longer session:

Still nothing. And the reason is scale. Vercel's before numbers are 21.5 GB for the Vercel dashboard and 4.6 GB for nextjs.org. Our dev server peaks around 1.5 to 2 GB, an order of magnitude below where their baseline even begins. Eviction reclaims memory after cache snapshots under sustained pressure, in long sessions. A 21-route site driven for a minute never creates that pressure.

The storefront told the same story, across three versions:

Overlapping again, and this project peaks around 0.5 to 0.8 GB. Even our 4.5x bigger codebase is an order of magnitude below where Vercel's 4.6 GB and 21.5 GB baselines start.

The claim may well hold. We are reporting that it is untestable at this size, and that if dev memory is a daily annoyance on your app, you are the case we could not represent. Two things follow for you: check whether you are already on 16.1 or later, since you may have banked half the win a while ago, and measure a long session rather than a cold start.

Handling load: 7% on one app, 52% on the other

We ran next start and pointed autocannon at the homepage with 20 connections, three interleaved pairs.

A consistent ~7% improvement, reproducible across every pair. Average latency improved by about a millisecond.

Vercel claims up to 22%. We got about a third of that, and the docs say why: the change replaces web streams with native Node.js streams in the App Router rendering layer. Our site prerenders 22 of 23 routes, so under load it is mostly handing back finished HTML and barely exercising the rendering path that got faster. Getting a third of the benefit while doing almost none of the work the optimization targets is a reasonable outcome.

Treat our 7% as a floor. An app doing genuine per-request rendering has far more to gain here than we did.

So we went and measured it on the storefront, which is the app this claim is actually about. We started its backend locally, rebuilt against real data, and ran the same interleaved A/B against two routes that render per request.

Between 46% and 52% more requests per second, with latency down about 25%. On an app that actually exercises the rendering path we measured more than double Vercel's 22%, and both interleaved pairs agree to within 4%.

One check worth stating, because it is the obvious objection: was the backend the bottleneck? No. If it had been, both Next versions would have converged on the same figure. They shared the same backend, the same data and the same cookie, and differ by 50%. What moved was the rendering.

So the same release gives one app 7% and another 52%, and the mechanism explains both. Native Node streams replace web streams in the App Router rendering layer. Serve prerendered HTML and you barely touch it. Render every route per request and you get the whole thing. Whether 16.3 is worth 22% to you depends entirely on how much rendering your app does.

What a real 15 to 16 migration cost us

The storefront crossed a major version, so it is the better test of migration pain.

The build passed on the first attempt with zero code changes, on a real e-commerce storefront with a cart, a checkout and Stripe. No codemod needed for a green build. That genuinely surprised us.

Then we looked underneath it, and the green build proved less than it looked. This project sets typescript.ignoreBuildErrors: true, as plenty of projects do. Running tsc --noEmit separately, with an identical tsconfig.json in both runs:

All 32 new ones are the same breaking change. revalidateTag(tag) now takes a second argument:

1
2
const cartCacheTag = await getCacheTag("carts")
revalidateTag(cartCacheTag)   // TS2554: Expected 2 arguments, but got 1

The docs are precise about what one argument now means: it still works, but it is deprecated and behaves like { expire: 0 }. Stale content is never served, so the next request blocks on a full revalidation instead of getting stale-while-revalidate. Nothing breaks. The app takes the slow path on every cart mutation, and a pipeline that ignores type errors will never report it. The fix is revalidateTag(tag, "max"), or in a Server Action.

Next 16 also rewrote tsconfig.json on its own, and two of the changes are semantic rather than cosmetic:

1
2
3
4
- "moduleResolution": "node",
+ "moduleResolution": "bundler",
- "jsx": "preserve",
+ "jsx": "react-jsx",

It reformatted every array in the file at the same time, so the real changes arrive buried in a noisy diff. next-env.d.ts picked up a new import too. Both files are committed in most repos, so this lands in somebody's review whether they asked for it or not.

So the upgrade is genuinely cheap, and "the build is green" is not the same as "the migration is done". Run tsc yourself, especially if your config ignores build errors.

One thing that did nothing

Our project runs the React Compiler through Babel, which is exactly the case experimental.turbopackRustReactCompiler targets. Vercel reports 34% faster cold and 46% faster warm time-to-ready-page on v0.

No measurable difference at 50 source files. Vercel's numbers come from v0, a very large app, and they note the gains assume you have moved off Babel entirely. Filed under "revisit on a bigger codebase".

Should you upgrade?

Yes. Both upgrades produced a green build with zero code changes, including the major version jump from 15.3.9 on a real e-commerce storefront. No codemods required. That is about as cheap as a framework upgrade gets.

It is also not the whole job. On the marketing site the upgrade needed no code, but the biggest improvement in the release stayed invisible until we changed the deployment pipeline. On the storefront a green build hid 32 deprecated calls. In both cases the improvements everyone quotes only arrived once we went and collected them.

The checklist we wish the benchmark plan had started with:

  1. Persist .next/cache between builds. Cache mount in Docker, cache entry in CI. Without this the biggest win in 16.3 is precisely zero, and it fails silently. Highest value, lowest effort, do it first.
  2. Move to TypeScript 7. Roughly 7x faster type checking, and once the build cache lands, type checking is what your build time actually consists of. Check your package manager first, Yarn 4 currently cannot install it.
  3. Check which version you are coming from. It changes which win you get. Coming from 15, the big one is cold builds, and that is the webpack to Turbopack switch rather than anything in 16.3. Coming from 16.1 or 16.2, cold builds will not move and the warm build cache is the whole story. Half the dev memory work also shipped back in 16.1.
  4. Run tsc --noEmit yourself after upgrading. A green next build is not proof, and if your config sets ignoreBuildErrors it is barely evidence. That is how we found 32 deprecated revalidateTag calls that nothing else reported.

Instant Navigations is a separate decision with its own timeline, and part 2 is about what it costs.

And calibrate against your own app. Warm builds were the biggest win on both projects, and the one number that held steady across them: 15.3x on 50 files, 13.8x on 223. Dev memory was unmeasurable on both, because even the larger one runs an order of magnitude below where Vercel's benchmark starts. And SSR throughput turned out to depend so completely on your rendering profile that a single headline number for it is close to meaningless: 7% on one of our apps, 52% on the other.

Measure your own, interleave the runs, and read the docs before you write the benchmark rather than after. We did it in the wrong order and it cost us two false conclusions and a rewrite.

One small housekeeping note: installing 16.3.4 writes AGENTS.md and a CLAUDE.md into your repo root, containing version-matched instructions for AI coding agents. They appear without being asked for, and the docs are clear that you should commit them, since next dev re-creates the block anyway.

Questions we were asked while writing this

Is Next.js 16.3 worth upgrading to?

Yes. On both codebases the upgrade produced a green build with zero code changes, including a major version jump from 15.3.9 on the e-commerce storefront. The improvements are a separate piece of work from the version bump: the build cache needs your pipeline to persist .next/cache, and Cache Components is a migration rather than a flag.

How much faster are Next.js 16.3 builds?

Cold builds do not change at all between 16.1.6 and 16.3.4. Repeat builds do: the compile step went from 2.9 s to 0.19 s on a 50-file site and from 4.4 s to 0.32 s on a 223-file storefront, 15.3x and 13.8x. If you are coming from Next 15 you also get a 2.7x faster cold build, but that is Turbopack replacing webpack in 16.0, not anything in 16.3.

Why did my Next.js 16.3 build times not improve?

Most likely your pipeline is discarding the cache. The Turbopack FileSystem cache lives in .next/cache, and a container build starts from a clean layer. On our project, deleting only that directory took the compile step from 200 ms back to 3.5 s. Add a BuildKit cache mount or a CI cache entry for it.

Does Next.js 16.3 really serve 22% more requests?

It depends entirely on how much your app renders per request. The change swaps web streams for native Node streams in the App Router rendering layer. Our mostly prerendered marketing site gained 7%. Our storefront, which renders nearly every route per request, gained 46% to 52%.

About the author

Piotr Zarow is CEO at Dev and Deliver, a Krakow-based software house working with senior React, Next.js, Node and NestJS engineers. He ran these benchmarks himself, on two client codebases the company builds and maintains, in September 2026 on Node 24 with Yarn 4. The method is described in full above so you can reproduce it against your own app rather than take the numbers on trust.

Planning a Next.js upgrade, or trying to work out whether a release is worth the migration effort on your codebase? We are a Krakow-based software house working with senior React, Next.js, Node and NestJS engineers, and this kind of measure-first assessment is how we approach it. Get in touch and tell us what you are building.

Piotr Żarów

CEO

Share this post

Related posts

Want to light up your ideas with us?

Kickstart your new project with us in just 1 step!

Prefer to call or write a traditional e-mail?