Zipp benchmarks
This is the canonical home for Zipp's performance numbers. Every table below comes from a checked-in JSON capture with engine hashes, competitor hashes, per-repetition schedules and bootstrap replicates, so anyone can re-run it and anyone can cite it. Ratios are Zipp divided by the competitor: lower is faster.
Headline
- Zipp/Node, all 30 rows, geometric mean
- 0.728×
- Zipp/Bun, all 30 rows
- 0.594×
- Zipp/Deno, all 30 rows
- 0.460×
- Rows where Zipp's median beats Node's
- 21 / 30
95% bootstrap interval 0.723 to 0.730
0.591 to 0.598
0.458 to 0.464
9 rows are slower; they are listed below
Capture: 2 September 2026, engine commit 8229b3fc, a clean profile-guided-optimization build recorded as zipp 0.0.12 (the README labels the same binary 0.0.13; this page uses the identity the artifact itself recorded). Native Windows x86-64, four engines, 15 repetitions per row, exact output on every observation.
Methodology
- Cold process time. Every observation is a fresh process timed with a wall clock around spawn and exit. A paired empty launch of the same engine runs immediately before each full launch; the headline uses the cold time, and the launch-adjusted time is reported as a diagnostic.
- No warm-up and no
--jitlessgames. Every engine runs with its production JIT and an empty environment; any inherited runtime control such asNODE_OPTIONSorZIPP_NOJITmarks a run diagnostic-only. - Counterbalanced schedule. A seeded engine order rotates once per repetition so each engine occupies every position; the per-repetition order is recorded in the artifact.
- Intervals. 95% percentile bootstrap with 10,000 replicates, described in the artifact as descriptive, not a hypothesis test. Suite geometric means use a cluster bootstrap by repetition. "Win" below means a Bonferroni-adjusted exact one-sided paired sign test at 0.05 across all comparisons (39 for the normal suite, 51 for hostile).
- Correctness first. Standard output is compared byte for byte against the reference on every repetition. A single mismatch disqualifies the capture.
- Publication gate. Full Node, Bun, Deno, Zipp order with Node as baseline; the complete frozen suite; at least 15 repetitions and 10,000 bootstrap samples; a clean,
HEAD-matching, provenance-stamped PGO binary.
| Item | Value |
|---|---|
| CPU | AMD Ryzen 9 9950X3D (AMD64 Family 26 Model 68), High performance power plan. RAM is not recorded. |
| OS | Windows 11, build 10.0.26200 |
| Zipp build | rustc 1.92.0 (LLVM 21.1.3), x86_64-pc-windows-msvc, --no-default-features release: opt-level=3, lto=fat, codegen-units=1, panic=abort, -Ctarget-cpu=x86-64 (not native), -Cprofile-use PGO, rust-lld |
| Zipp binary | zipp 0.0.12, SHA-256 bf9fddab…dc9986, source 8229b3fc, not dirty |
| Node | v24.12.0, SHA-256 2ffe3acc… |
| Bun | 1.3.14 via bun run, SHA-256 0187f68d… |
| Deno | 2.6.10 stable via deno run, SHA-256 49e2f75a… |
| Harness | tools/bench.py and tools/bench_hostile.py, Python 3.11.6, seed 0x5A172026, 300-second timeout |
Startup
Median empty-process launch, the paired baseline for every row:
| Engine | Launch |
|---|---|
| Zipp | 7.4 |
| Node | 30.4 |
| Bun | 43.3 |
| Deno | 82.6 (81.8 in the normal capture, 83.3 in the hostile one) |
Normal suite: 13 rows
Ten headline workloads plus three architecture diagnostics that are deliberately kept out of the headline geometric mean. Cold medians in milliseconds; the interval is the 95% bootstrap on the Zipp/Node ratio; an asterisk marks a Bonferroni-adjusted sign-test win over Node.
| Row | Node | Bun | Deno | Zipp | Zipp/Node | Zipp/Bun | Zipp/Deno |
|---|---|---|---|---|---|---|---|
| async-promise-chain | 333.7 | 369.2 | 358.9 | 372.1 | 1.118 [1.103, 1.132] | 1.009 | 1.041 |
| class-prototype-hot | 296.7 | 332.5 | 329.5 | 226.1 | 0.766* [0.762, 0.774] | 0.680 | 0.690 |
| json-large | 269.7 | 192.5 | 322.0 | 271.0 | 1.005 [0.984, 1.027] | 1.408 | 0.838 |
| map-set-heavy | 783.7 | 855.1 | 1264.4 | 671.6 | 0.837* [0.816, 0.869] | 0.786 | 0.521 |
| markdown-render | 268.5 | 207.3 | 315.7 | 208.9 | 0.767* [0.762, 0.787] | 1.000 | 0.653 |
| parse-large-js | 272.8 | 230.3 | 295.8 | 233.0 | 0.859* [0.837, 0.864] | 1.019 | 0.792 |
| polymorphic-objects | 327.7 | 331.1 | 339.8 | 309.2 | 0.942* | 0.931 | 0.904 |
| regex-log-scan | 478.0 | 564.2 | 459.6 | 447.9 | 0.938* | 0.792 | 0.978 |
| sparse-array | 81.1 | 112.8 | 129.3 | 73.2 | 0.908* | 0.645 | 0.564 |
| typedarray-math | 199.8 | 913.8 | 169.9 | 144.1 | 0.719* | 0.157 | 0.846 |
| polymorphic-objects-v2 (diagnostic) | 81.1 | 87.4 | 131.4 | 24.5 | 0.302* | 0.278 | 0.186 |
| property-ic-shapes (diagnostic) | 265.4 | 157.6 | 318.7 | 9.6 | 0.036* | 0.061 | 0.031 |
| sparse-array-v2 (diagnostic) | 171.1 | 366.3 | 183.9 | 99.2 | 0.585* | 0.271 | 0.538 |
| Set | Geometric mean | 95% interval |
|---|---|---|
| Headline 10 | 0.878× | [0.875, 0.884] |
| Diagnostics 3 | 0.186× | [0.183, 0.188] |
| All 13 | 0.614× | [0.611, 0.617] |
Note what parse-large-js measures: a hand-written userland tokenizer over two megabytes of synthetic source. It does not benchmark Zipp's own parser. The diagnostic rows isolate the inline-cache cliff at nine receiver shapes and the sparse-array gap-size axes; they are useful for engineering and would flatter the headline, which is why they are excluded from it.
Hostile suite: 17 rows
The hostile corpus asks how gracefully Zipp performs when JavaScript stops looking like an ideal reducer: six baseline-and-stressor pairs plus five standalone applications, including a React-style reconciler, a warm HTTP router, a bytecode interpreter and the unmodified [email protected] package.
| Row | Zipp/Node | Row | Zipp/Node |
|---|---|---|---|
| calls-baseline | 0.485 | calls-closures | 1.118 |
| shapes-stable | 1.241 | shapes-megamorphic | 1.245 |
| types-stable | 0.514 | types-churn | 0.740 |
| branch-control | 0.828 | throw-catch | 0.498 |
| allocation-ephemeral | 0.360 | allocation-survival | 1.559 |
| async-burst | 0.613 | async-lived | 1.005 |
| reactish-reconcile | 1.578 | warm-router | 1.520 |
| bytecode-vm | 0.978 | module-hot-graph | 0.401 |
| npm-nanoid | 0.975 |
Ordinary geometric mean 0.829× Node [0.820, 0.833], 0.647× Bun, 0.419× Deno; category-balanced 0.862× Node. The suite's aspirational gate (category-balanced at most 1.05×, no category above 1.15×, no row above 1.50×) fails on both the category and the row bounds.
Where Zipp loses
Nine of thirty rows have a Node point gap: reactish-reconcile 1.578×, allocation-survival 1.559×, warm-router 1.520×, shapes-megamorphic 1.245×, shapes-stable 1.241×, calls-closures 1.118×, async-promise-chain 1.118×, and two rows whose intervals cross parity, async-lived 1.005× and json-large 1.005×. The pattern is consistent: long-lived allocation, megamorphic property access and promise-heavy code are where V8's optimizing tiers still win.
Against Bun, Zipp loses json-large (1.408×), throw-catch (1.636×), allocation-survival (1.198×), reactish-reconcile (1.041×), parse-large-js (1.019×) and async-promise-chain (1.009×). Bun is the absolute fastest engine on json-large, markdown-render and parse-large-js; Deno is fastest on typedarray-math. Against Deno, Zipp loses throw-catch (1.470×) and async-promise-chain (1.041×).
WebAssembly, Python and GPU
None of the numbers above apply to the WebAssembly build, which is an interpreter. The only WebAssembly comparison in the repository, against QuickJS-NG v0.16.2, is marked not publishable: only five hostile rows were comparable, QuickJS-NG led four of them, and Zipp led warm-router at 0.476×. Module size is a settled fact rather than a benchmark: 5,558,860 bytes raw and 1,248,649 Brotli for Zipp's v0.0.12 artifact against 1,528,293 and 417,087 for QuickJS-NG, 3.59× raw and 2.96× on the wire.
Five WebAssembly interpreter cliffs were closed on 5 September 2026 with paired before-and-after measurements on one machine: a 64K non-ASCII charCodeAt loop 4,462 → 3.2 ms, a tokenizer 9,828 → 8.2 ms, a slice loop 449 → 4.3 ms, join with 300K retained strings 8,423 → 138 ms, and a new Function property loop 20.9 → 14.8 ms.
There are no published Python-frontend benchmark tables beyond the three micro-timings on the Python page, and no GPU performance numbers at all; the GPU work is validated for correctness only.
The 11 September capture, and why it is slower
A newer publishable capture exists at commit 14770703 (zipp 0.0.15, 11 September 2026): headline ten 1.003× Node [0.995, 1.009], all 13 rows 0.677×, hostile ordinary 0.875×, with bytecode-vm at 3.59× Node. A paired A/B against the audited baseline e6e0f65d puts that baseline within 1% of the new build on every row, so the regression opened between 8229b3fc and e6e0f65d. The suspect is the v0.0.15 change that made specification-order method calls the default: the fused CallMethod lowering was observably wrong for a getter or a proxy trap and is now used only where argument shapes provably cannot observe the order. No headline is claimed for 14770703; the trade was correctness for speed, and the ledger says so.
- 1.90× Node, headline ten
First attributable default capture; interpreter-only smoke at 6.42×.
- PGO adopted at −13.3%
1.749× to 1.451× in one build change; two more waves reach 1.330× the same day.
- Nursery on by default: 1.212×
- First capture under 1.20×
typedarray-math reaches Node parity.
- 0.9695×: Node parity crossed
Wave 19 decomposed the three rows that were 60% of the remaining gap.
- Hostile suite under parity at 0.961×
- A 2.16× regression, reversed
A de-fused call lowering; root-caused and reverted the same day.
- 0.878× headline, 0.728× all-30: the public capture
- 1.003× headline after strict call order
Correctness kept; speed to be recovered.
Reproduce it
& 'C:\Program Files\Git\bin\bash.exe' tools/pgo.sh # profile-guided build of target/release/zipp.exe
New-Item -ItemType Directory -Force target\bench-results | Out-Null
python tools\bench.py --zipp target\release\zipp.exe --engines node,bun,deno,zipp `
--reps 15 --bootstrap-samples 10000 --json target\bench-results\real13.json
python tools\bench_hostile.py --zipp target\release\zipp.exe `
--reps 15 --bootstrap-samples 10000 --json target\bench-results\hostile17.json
python -m unittest tools.test_bench- bench/real13_8229b3fc_pgo_2026-09-02.json, the normal-suite capture.
- bench/hostile/head_clean_8229b3fc_pgo_2026-09-02.json, the hostile capture.
- bench/real13_14770703_pgo_2026-09-11.json and the hostile pair, the later capture.
- bench/README.md, the suite map and publication gate; bench/hostile/README.md and manifest.json.
- PERF_ROADMAP.md, the checkable roadmap, and the archived performance ledger, 881 KB of experiments including the refuted ones.