One machine, one app, same database, same day. Every number below comes from a reproducible harness.
Bars are roughly to scale — the nextrs bar is a sliver because the gap is that large. Real values + ratios labeled.
13 dimensions of the dev loop. 11 favor nextrs; 2 favor the JS toolchain (stated plainly — see §04).
| Dimension | Next.js 16 dev | nextrs | Edge |
|---|---|---|---|
| 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 TTI | 2.2 ms (rel 1.82) | ~300× |
| Dev-server RAM | 16–27 GB | 46→75 MB | ~210–600× |
| Min RAM to run at all | ~14 GB (OOM <) | <48 MB | ~300× |
| Production build | 238 s → 5.7 GB | 46 s → 47 MB | ~5× / 120× |
| Type-check the project | tsc 117 s (12 GB heap) | cargo check 27.5 s | ~4× |
| ~1,200 DB-backed tests | vitest+Prisma ~141 s/shard | nextrs 1.6 s | ~85× |
| Incremental compile (1 edit) | — | ~1.9 s | crate-scoped |
| Boot the server | several s | 30 ms | fast |
| React hot-edit (HMR) | 0.3–0.7 s ✅ | 2.5 s (rebuild+reload) | Next |
| Lint | Biome 3.8 s ✅ | clippy 17.7 s | Next |
What does it cost to run a thousand database-backed tests? We ran both — same machine, same kind of test (fixtures → real Postgres → insertions).
| nextrs | current app — real CI shard | |
|---|---|---|
| Measured run | 1,201 tests · 1.6 s | 1,306 tests · 140.8 s wall |
| ≈ per-test wall-clock | ~1.3 ms | ~108 ms (~85×) |
| Range across the 12 shards | — | 140–379 s each |
| Isolation model | one shared PG · txn rollback | per-worker DB · module graph reloaded per file |
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:
| Phase | What it is | CPU-time (all workers) |
|---|---|---|
| collect | loading + evaluating each file's module graph (imports, app code, the generated Prisma client) | 445 s |
| transform | transpiling TS → JS, per file | 30 s |
| tests | the actual assertions, including the real Postgres queries | 78 s |
| setup (DB) | vitest setup + DB provisioning | 2.7 s |
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.js | nextrs |
|---|---|---|
| Type-check | tsc 117 s · 12 GB heap | cargo check 27.5 s |
| ~1,200 DB-backed tests | vitest+Prisma ~141 s/shard | cargo-nextest 1.6 s |
| Production build | 238 s → 5.7 GB | 46 s → 47 MB |
| Lint | Biome 3.8 s | clippy 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.
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.
| Layer | Ported | How deep |
|---|---|---|
| Structure — routes, schema, dispatch | 100% | 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.
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.
--timings build:
| Code type | ms / LOC |
|---|---|
Schema — db-models (models + enums) | 1.02 |
Business logic — all gen-* | 0.77 |
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.| 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.
The JS dev server cannot run with less than ~14 GB of memory — capped with cgroups, kernel-confirmed.
| Engine | Cap | Result |
|---|---|---|
| nextrs | 256 MB | served 8/8 pages · 35 MB used |
| nextrs | 48 MB | served the whole app · floor is under 48 MB |
| Next dev | 4 GB | kernel-OOM-killed on the first page · 0/8 |
| Next dev | 8 GB | kernel-OOM-killed · 0/8 — 8 GB still isn't enough |
| Next dev | 16 GB | works: 8/8, but peak 13.9 GB → needs ~14 GB to render |
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).
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.
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.
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.
The numbers above are the case. These are the five things I actually walked away believing.
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.
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.
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.
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.
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.
docs/benchmarks/.