Developer-experience benchmark · measured, reproducible

The dev loop, measured:
Next.js vs the Rust port (nextrs)

One machine, one app, same database, same day. Every number below comes from a reproducible harness.

Intel Ultra 9 285K · 24 cores · 125 GiB · Next 16.2.6 (Turbopack) / React 19.2.3 · same Postgres · 2026-06-27
Open a page you haven't visited yet today
7.1 s Next dev
3.6 ms nextrs
~1,975× · up to 22 s in Next · in a real browser: 18.7 s vs 45 ms
In Next dev every route compiles on first visit. nextrs compiles all 205 routes into the binary once (31.5 s), then every page is a few ms — forever.
Memory to run our own app in dev
~14 GB Next dev needs
<48 MB nextrs runs in
kernel-OOM-killed at 4 GB AND 8 GB · works only at a 16 GB cap (13.9 GB peak)
Measured with cgroup caps, kernel-confirmed. Next dev needs a 32 GB machine to be comfortable; nextrs fits ~300× over.

01At a glance

Bars are roughly to scale — the nextrs bar is a sliver because the gap is that large. Real values + ratios labeled.

Open an unseen page (cold, in a real browser)lower is better
nextrs — 45 ms  (~400× faster)
Dev-server RAM (load-tested)lower is better
nextrs — 46→75 MB  (~210–600× less)
Production build → deployablelower is better
cargo build --release — 46 s → 47 MB binary

02Every dimension, head to head

13 dimensions of the dev loop. 11 favor nextrs; 2 favor the JS toolchain (stated plainly — see §04).

DimensionNext.js 16 devnextrsEdge
First open of a page (35-route census, median)7.1 s (≤22 s)3.6 ms~1,975×
Warm page (already compiled)682 ms TTI2.2 ms (rel 1.82)~300×
Dev-server RAM16–27 GB46→75 MB~210–600×
Min RAM to run at all~14 GB (OOM <)<48 MB~300×
Production build238 s → 5.7 GB46 s → 47 MB~5× / 120×
Type-check the projecttsc 117 s (12 GB heap)cargo check 27.5 s~4×
~1,200 DB-backed testsvitest+Prisma ~141 s/shardnextrs 1.6 s~85×
Incremental compile (1 edit)~1.9 scrate-scoped
Boot the serverseveral s30 msfast
React hot-edit (HMR)0.3–0.7 s ✅2.5 s (rebuild+reload)Next
LintBiome 3.8 s ✅clippy 17.7 sNext
nextrs wins JS toolchain wins

03CI

What does it cost to run a thousand database-backed tests? We ran both — same machine, same kind of test (fixtures → real Postgres → insertions).

nextrscurrent app — real CI shard
Measured run1,201 tests · 1.6 s1,306 tests · 140.8 s wall
≈ per-test wall-clock~1.3 ms~108 ms (~85×)
Range across the 12 shards140–379 s each
Isolation modelone shared PG · txn rollbackper-worker DB · module graph reloaded per file

Where the time actually goes — Prisma and the DB are not the bottleneck

vitest runs many test files in parallel across worker threads, so its phase totals are CPU-time summed across all workers — that's why they add up to far more than the 141 s of wall-clock. Don't read them as seconds on a stopwatch; read the ratio between phases — that's what shows where the work goes. On shard 4:

PhaseWhat it isCPU-time (all workers)
collectloading + evaluating each file's module graph (imports, app code, the generated Prisma client)445 s
transformtranspiling TS → JS, per file30 s
teststhe actual assertions, including the real Postgres queries78 s
setup (DB)vitest setup + DB provisioning2.7 s

The real problem: the module graph, recomputed per file

The database is 2.7 s and the test bodies (with real queries) are 78 s — both dwarfed by collect at 445 s. Resolving and evaluating this app's module graph is already expensive in TypeScript, and the way vitest runs tests makes it worse: it re-computes that graph from scratch for every test file, in every worker, every run. The cost scales with the app's dependency surface, not with the number of assertions.

CI gate (billed wall-clock)Next.jsnextrs
Type-checktsc 117 s · 12 GB heapcargo check 27.5 s
~1,200 DB-backed testsvitest+Prisma ~141 s/shardcargo-nextest 1.6 s
Production build238 s → 5.7 GB46 s → 47 MB
LintBiome 3.8 sclippy 17.7 s

Honest caveat: a true full-suite comparison would require porting all 33k tests to Rust; we didn't. The per-file + per-gate + runner-memory numbers are the apples-to-apples ones — and they're what multiply into the monthly bill.

Read this with §02 — the honest qualifier

How comparable are these two apps?

The numbers above only mean something if the two apps are the same app. They are the same product — and the port is deliberately structurally complete and behaviorally partial, applied uniformly across all ~80 bounded contexts.

LayerPortedHow deep
Structure — routes, schema, dispatch100%All 205 routes · 404 models / 360 enums · ~1,935 procedure signatures wired & type-checked. The React UI (app/ + components/, ~768k LOC) is reused byte-for-byte, zero-copy — not rewritten.
Behavior — real handler logic~40%~800 of ~1,935 procedures have real sqlx bodies (org isolation, actual logic); the rest are typed stubs/proxies. Spread evenly — most contexts have some real, some stubbed.
Tests — converted from the TS suite~7%1,532 of 22,680 backend cases (851 active + 681 documented #[ignore] PORT-GAPs). By design: 8–15 representative cases per context to prove the conversion pattern, not one-shot parity.

The 681 PORT-GAPs are the honest move — they document the behavior the Rust side stubs instead of faking a green. Why only 7%? Full conversion was deferred because the old TS test pattern couldn't run at scale (per-test pools + shared-DB writes deadlocked on connection exhaustion). The txn-rollback harness, crate split, and cargo-nextest built here are exactly what unblock it — the same machinery that runs 1,200 DB-backed tests in 1.6 s (§03). The remaining 93% is now a feasible swarm pass, not lost work.

04What if it was a full port?

The port is ~123k LOC; the real TS app is ~1.37M first-party lines — so it is smaller. But the growth from here is favorable, because the expensive part of a Rust build is already done. The schema — the 404 models and 360 enums — is the single heaviest, most-expensive-per-line crate and the longest serial pole in the build, and it's 100% ported. What's left to fill in is business logic, which compiles cheaper per line and across ~40 crates in parallel.

The expensive part is the schema — and it's finished
Per-crate compile cost from a real --timings build:
Code typems / LOC
Schemadb-models (models + enums)1.02
Business logic — all gen-*0.77
The schema is ~1.3× costlier per line, is the heaviest single crate (15 s) and the serial bottleneck — and it doesn't grow with a full port. Business logic is cheaper and lands in leaf crates that compile in parallel.
And the overall slope is near-flat
We injected realistic handler-shaped code (async + Box::pin + generics + json) to inflate the workspace to 158k → 196k → 278k LOC and re-measured: ~24 ms per 1,000 LOC. A --timings split shows why it barely moves — deps are 63% of the work but compiled once and cached; only the first-party slice grows. Held constant: same machine, same Postgres, same React (byte-for-byte), nextrs in debug — biased against us. Caveat: the slope assumes handler-shaped code; heavy macros or deep generics compile slower.

What a full-size port's cold build would cost

From-scratch cold build (deps + all first-party)Projection
Today (~123k first-party LOC)34 s (measured)
Best case — full port, handler-shaped code spread across crates, parallelizes on multi-core~45 s
Worst case — code concentrates in hub crates / heavy generics & macros, parallelism degrades~90 s – 2 min

Either way it's a once-in-a-while event (clean checkout or dependency bump). The daily loop is the ~2 s incremental — size-independent, because the crate split recompiles only the touched crate. For scale: next build is already 238 s at today's size, and grows too.

Wherever this comparison is imperfect, it runs against nextrs — the beefy box flatters Next's heavier compile/memory, and nextrs's numbers are the understating debug build. So the measured gaps are a floor, not a ceiling.

05The memory cliff

The JS dev server cannot run with less than ~14 GB of memory — capped with cgroups, kernel-confirmed.

EngineCapResult
nextrs256 MBserved 8/8 pages · 35 MB used
nextrs48 MBserved the whole app · floor is under 48 MB
Next dev4 GBkernel-OOM-killed on the first page · 0/8
Next dev8 GBkernel-OOM-killed · 0/8 — 8 GB still isn't enough
Next dev16 GBworks: 8/8, but peak 13.9 GB → needs ~14 GB to render

06Cold starts & function size

Cold starts are what the user feels. We measured the real Next.js app live on Vercel; the nextrs runtime figures below are expectations, not Vercel measurements (we haven't deployed it there).

The production Next.js app cold-started in 11.2 seconds — live, on Vercel

First request to the real Next.js app on an idle instance: 11.2 s to first byte, then 0.36 s warm. That multi-second cold start is the dead-click experience.

Why it's so slow: module resolution — the same beast as §03's tests

The dominant cost in the Next cold start is the same thing that dominates the test suite: resolving and evaluating a huge module graph at request time. The way this app is structured gives Node an enormous import tree to walk and parse before it can serve a byte. A compiled Rust binary has no module resolution at runtime — the graph was linked at build time — so its app-init contribution is ~9 ms locally (binary loads, tokio starts, DB pool connects). Same root cause, two symptoms: slow cold starts and slow tests.

Function size — why Next hits Vercel's 250 MB limit and nextrs won't

Next.js — per-route functions
Each route becomes a serverless function bundling React + that route's node_modules; shared deps duplicate across functions → they creep toward the 250 MB unzipped ceiling. "React in every function" is the tax you're paying.
nextrs — one function
The whole app is one function = the Rust binarymeasured 59 MB raw / 47 MB stripped, the entire 205-route app, regardless of route count. React isn't in the function — it's static JS on the CDN. No per-route duplication, ~5× under the 250 MB wall, and it doesn't grow with the route count the way per-route bundles do.

07What Next still wins (so you can trust the rest)

Two loops favor the JS toolchain. We say so plainly.

React hot-edit (HMR): Next 0.3–0.7 s vs nextrs 2.5 s (rebuild + reload). This is the one loop Next wins — and it's the loop that already hurt least.

Lint: Biome 3.8 s vs clippy 17.7 s. Biome is itself Rust; it's just very good.

The trade: nextrs gives up ~2 s on the cheap loop to erase 7–22 s on the expensive one — and ~210–600× the memory. That's the whole bet.

08What it costs (and what it unlocks)

The daily tax (compounding)
~30 cold page-opens/day × ~7 s ≈ 3.5 min/dev/day of literal spinner → ~7 engineer-weeks/year across 20 devs, before the dropped-intention cost of each flow-break.
Hardware
Next dev's ~14 GB floor forces 32 GB machines or cloud dev boxes (~$60–280/mo/dev). nextrs at <48 MB removes "can my machine run our own app" from the equation: ~$0.
CI (billed wall-clock)
Type-check 117 s→27.5 s, ~1,200 DB tests 141 s/shard→1.6 s, build 238 s→46 s — cheaper per run and on smaller runners.
The unlock that's foreclosed today
A live preview env per open PR and agent-scale dev — dozens of full app instances per box, each able to run the app to verify its own change. Impossible at 27 GB; trivial at 48 MB.

09My takeaways

The numbers above are the case. These are the five things I actually walked away believing.

1 · The process itself is the headline

I was extremely hands-off on this, and I'm genuinely amazed at the results for how little I had to put in. A full-breadth port of a 205-route app — measured, playable, honestly benchmarked — came together with a fraction of the effort I'd have budgeted. That's a data point about how we build now, not just about the framework.

2 · The JS module graph will keep biting us

Module resolution is the one thing that fought back the whole way — the runtime process.env shims, the bare-import roots, the cost of walking the graph itself. It's the same beast behind slow cold starts and slow tests, and it was the hardest part to wire even from the Rust side. I don't expect that edge to fully go away; it's the area to keep watching.

3 · The real prize is headroom, not seconds

Shaving a dev loop is nice, but it's not the point. The point is what the speed and the <48 MB footprint unlock: automated testing at a scale we can't afford today, dozens of app instances per box, and the freedom to stop spending so much of our attention on performance. Fast enough stops being a constraint we design around.

4 · In the AI era, why is the language the one thing we won't reconsider?

We're rethinking almost everything about how we develop for AI — tools, workflows, how much a single engineer can hold. It's strange that the language we write in is the one layer we treat as fixed. If we're willing to question the how, the what we build it in deserves the same question.

5 · Rewrites have been going well for us

We rewrote the mobile app and it went really well — and a lot of companies are rewriting right now, so the ground is well-trodden. My whole belief here is simple: a rewrite like this is what lets us move faster later. The point of doing it isn't this quarter; it's the velocity it buys for every quarter after.

Methodology & honesty. All timings wall-clock, medians of N=3 where applicable; cold compiles are one-shot. nextrs runtime numbers are from the DEBUG build (release is faster + lighter — a conservative floor). Same app, same Postgres, same machine; Next pages were real error-free renders. Reproducible harnesses + full raw data in docs/benchmarks/.
cold page 7.1 s → 3.6 msRAM ~14 GB → <48 MBbuild 238 s → 46 stypecheck 117 s → 27.5 s~1.2k DB tests: 141 s shard → 1.6 sHMR: Next winslint: Next winscompile slope ~24 ms/1k LOC to 278k