163×
the JavaScript Next.js ships versus the lightest
non-zero stack (Astro) — for the identical interaction: a text + tag
filter on the index page.
JS shipped for the same page
native / BATCH
2kb
native + DPU
3kb
Astro
744b (inline)
Next.js
118kb (inline)
native / BATCH
0
native + DPU
0
Astro
0
Next.js
109kb (inline)
That spread is the whole thesis: the interaction is the same; the runtime each stack makes you ship for it is not. Astro's figure is its filter script inlined into the HTML; native's is one small external module — both are essentially the filter logic and nothing else. Next's is React runtime + hydration.
How to read this (the honest frame)
- Headline = network-independent, categorical metrics: JS shipped, request count, wire bytes, build-step yes/no, runtime-dependency count. These are robust and honest.
- TTFB / Load are local best-case corroboration, not proof. No network hop; real-world latency adds to every stack equally. Direction, never the headline.
- The comparison is not rigged: every target gets the same SEO/AEO head, so the parity columns below are ✓ across the board. Better SEO on one stack would be a confound, not a win.
- Where a stack ties or wins, it says so. On a mostly static page Astro's
no-runtime
<script>island ships comparably little to native — that honest tie is the point, not a problem.
Full numbers — index (/)
| Target | JS shipped | Wire | Requests | Render-blocking | TTFB (med) | Load (med) |
|---|---|---|---|---|---|---|
| native / BATCH | 2kb | 14kb | 3 | 1css/0js | 4ms | 12ms |
| native + DPU | 3kb | 15kb | 3 | 1css/0js | 3ms | 10ms |
| Astro | 744b (744b inline) | 5kb | 2 | 1css/0js | 11ms | 38ms |
| Next.js | 118kb (16kb inline) | 109kb | 7 | 1css/1js | 9ms | 128ms |
Full numbers — article
| Target | JS shipped | Wire | Requests | Render-blocking | TTFB (med) | Load (med) |
|---|---|---|---|---|---|---|
| native / BATCH | 0 | 7kb | 2 | 1css/0js | 3ms | 9ms |
| native + DPU | 0 | 7kb | 2 | 1css/0js | 3ms | 9ms |
| Astro | 0 | 3kb | 2 | 1css/0js | 4ms | 12ms |
| Next.js | 109kb (7kb inline) | 106kb | 6 | 1css/1js | 4ms | 66ms |
The facts the auditor can't measure
| Target | Build step | Runtime deps (direct) | node_modules | Production mode measured |
|---|---|---|---|---|
| native / BATCH | No | 1 (@tjakoen/batch) | 47M | bun server.ts — no build step (server IS the production artifact) |
| native + DPU | No | 1 (@tjakoen/batch) | 47M | bun server.ts — no build step; filter swaps a server fragment via native setHTMLUnsafe()/streamHTMLUnsafe() |
| Astro | Yes | 1 (astro) | 190M | astro build → astro preview (static output) |
| Next.js | Yes | 3 (next, react, react-dom) | 335M | next build && next start (client-component page is not pure SSG — see note) |
Parity — proof the comparison is fair
The same head on every target: index first, article second.
| Target | Title | Desc | Canonical | OG/Twitter | 1×H1 | JSON-LD | html lang | Semantics |
|---|---|---|---|---|---|---|---|---|
| native / BATCH | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| native + DPU | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Astro | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Next.js | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Target | Title | Desc | Canonical | OG/Twitter | 1×H1 | JSON-LD | html lang | Semantics |
|---|---|---|---|---|---|---|---|---|
| native / BATCH | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| native + DPU | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Astro | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Next.js | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Honest notes
- Next's asymmetry: the index is a
"use client"component, so it is not pure SSG — measured withnext build && next start, stated openly rather than forced into an unfair static export. Its JS is React runtime + hydration: the honest cost of reaching for a framework to do the filter. - Astro's tie: Astro's filter is an idiomatic
<script>island (bundled, no UI-framework runtime). On this static-ish page it ships about what native ships — the honest-win/tie case. - Compression asymmetry — conservative against native. The Astro/Next preview
servers gzip; native's
bun server.tsdoes not, so native's byte numbers are an upper bound — behind any CDN the gap only widens. - Inline vs external JS: the JS-shipped column adds inline
<script>source back in, so no stack hides code in the document. - Fresh context per page: each page is measured in its own browser (no shared cache), so a resource downloaded for the index can't make the article look lighter than it is.
Run it yourself
git clone https://github.com/tjakoen/framework-bench.git
cd framework-bench && bun install
bun run bench
Perf is corroboration. The bytes, the request count, and the build-step column are the argument.