Headline

Zipp/Node, all 30 rows, geometric mean
0.629×

normal 13 0.543×, hostile 17 0.704×

Zipp/Bun, all 30 rows
0.483×

normal 0.459×, hostile 0.502×

Zipp/Deno, all 30 rows
0.422×

normal 0.467×, hostile 0.390×

Rows where Zipp's median beats Node's
25 / 30

5 rows are slower; they are listed below

Capture: 29 October 2026, the 0.0.31 engine as a clean profile-guided-optimization build recorded as zipp 0.0.31 from source fcfb1934 (the release tag). Native Windows x86-64, four engines, 15 repetitions per row, exact output on every observation, and both artifacts record publishable:true. Every engine runs with its production JIT; Interpreter versus interpreter compares Zipp's interpreter with QuickJS and the JIT-off modes, and Python versus CPython its Python frontend. The all-30 figures weight the 30 rows equally, exp((13 ln G13 + 17 ln G17) / 30) of the two suites' geometric means, whose intervals are given with each suite. Zipp runs in its default mode, which in 0.0.31 sets up Node's globals for the program; on the normal rows that measured within 1% of the plain ECMAScript mode (ZIPP_NO_NODE=1).

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 --jitless games. Every engine runs with its production JIT and an empty environment; any inherited runtime control such as NODE_OPTIONS or ZIPP_NOJIT marks 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.
Environment recorded in the artifact
ItemValue
CPUAMD Ryzen 9 9950X3D (AMD64 Family 26 Model 68), High performance power plan. RAM is not recorded.
OSWindows 11, build 10.0.26200
Zipp buildrustc 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 binaryzipp 0.0.31, SHA-256 8380e0a6…e07452, source fcfb1934, not dirty
Nodev24.19.0, SHA-256 3602f2bb…
Bun1.3.14 via bun run, SHA-256 0187f68d…
Deno2.6.10 stable via deno run, SHA-256 49e2f75a…
Harnesstools/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:

Median process launch, milliseconds
EngineLaunch
Zipp9.1
Node32.3
Bun53.0
Deno84.9

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.

real13, capture fcfb1934, 29 October 2026
RowNodeBunDenoZippZipp/NodeZipp/BunZipp/Deno
async-promise-chain345.1380.0370.5238.10.700* [0.675, 0.718]0.6280.646
class-prototype-hot303.5344.1327.7229.00.759* [0.752, 0.765]0.6680.704
json-large288.9212.9335.2223.20.779* [0.758, 0.800]1.0450.683
map-set-heavy808.0856.01340.2548.60.679* [0.626, 0.689]0.6130.402
markdown-render283.5229.3324.7187.00.660* [0.655, 0.679]0.8080.582
parse-large-js277.6240.4290.7222.60.803* [0.793, 0.817]0.9250.774
polymorphic-objects334.6342.0338.2214.10.640* [0.622, 0.661]0.6250.639
polymorphic-objects-v284.496.8132.124.30.286* [0.282, 0.291]0.2480.188
property-ic-shapes259.5166.8322.111.10.043* [0.040, 0.045]0.0670.035
regex-log-scan488.4586.3482.2421.00.876* [0.837, 0.901]0.7160.856
sparse-array82.0110.3126.574.20.904* [0.886, 0.925]0.6790.600
sparse-array-v2174.1379.2188.997.40.560* [0.551, 0.569]0.2570.512
typedarray-math205.2926.7172.9141.40.688* [0.682, 0.703]0.1530.817
Aggregates versus Node
SetGeometric mean95% interval
Headline 100.744×[0.739, 0.750]
Diagnostics 30.190×[0.185, 0.193]
All 130.543×[0.538, 0.546]

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.

hostile17, capture fcfb1934, Zipp/Node cold ratio
RowZipp/NodeRowZipp/Node
calls-baseline0.472calls-closures1.122
shapes-stable0.927shapes-megamorphic1.038
types-stable0.503types-churn0.605
branch-control0.819throw-catch0.246
allocation-ephemeral0.370allocation-survival1.344
async-burst0.507async-lived0.667
reactish-reconcile1.217warm-router1.130
bytecode-vm0.924module-hot-graph0.403
npm-nanoid0.948

Ordinary geometric mean 0.704× Node [0.700, 0.709], 0.502× Bun, 0.390× Deno; category-balanced 0.738× Node. The suite's aspirational gate (category-balanced at most 1.05×, no category above 1.15×, no row above 1.50×) passes its category-balanced bound, fails the per-category bound (applications 1.217×), and passes the row bound (the worst row, allocation-survival, is 1.344×).

Where Zipp loses

Five of thirty rows have a Node point gap, all of them in the hostile suite: allocation-survival 1.344×, reactish-reconcile 1.217×, warm-router 1.130×, calls-closures 1.122×, shapes-megamorphic 1.038×. The pattern: long-lived allocation, closure-heavy application code and property access across many shapes are where V8's optimizing tiers still win. Against the previous capture (21 October 2026, zipp 0.0.28), all 30 rows went from 0.652× to 0.629× Node; the rows that improved most were async-lived (0.829× to 0.667×) and types-churn (0.693× to 0.605×) and bytecode-vm (1.050× to 0.924×) and async-burst (0.574× to 0.507×), and the one that moved most the other way was property-ic-shapes (0.037× to 0.043×).

Against Bun, Zipp loses json-large (1.045×). Against Deno, Zipp loses no row. On the normal suite Bun is the absolute fastest engine on json-large; Zipp is fastest on every other normal row.

Interpreter versus interpreter

The tables above compare production JITs. This one asks how fast Zipp is as a pure interpreter: the same PGO binary with ZIPP_NOJIT=1 ZIPP_NO_RX_JIT=1, which emits no native code (its JavaScript JIT and its regex JIT both off; the captures before 22 October left the regex JIT on), against Bellard's QuickJS 2026-06-04 and QuickJS-NG 0.17.0 (neither has a JIT), and against Node --jitless (V8's Ignition), Deno --v8-flags=--jitless and Bun with BUN_JSC_useJIT=0 (JavaScriptCore's LLInt). run_native.py staged identical sources for every engine, checked every output against Node's and ran 8 counterbalanced repetitions; cold medians in milliseconds, launch included. It is a diagnostic, not a canonical capture. This interpreter is what the WebAssembly build and the safe-sandbox profile run.

Zipp interpreter / engine, geometric means and rows won
EngineNormal 13Hostile 17All 30Zipp faster
QuickJS0.514× [0.512, 0.517]0.625× [0.612, 0.627]0.574×30 / 30
QuickJS-NG0.326× [0.321, 0.327]0.327× [0.324, 0.328]0.326×30 / 30
Node jitless1.010× [1.001, 1.017]0.843× [0.835, 0.847]0.911×21 / 30
Deno jitless0.964× [0.957, 0.968]0.733× [0.727, 0.739]0.825×24 / 30
Bun no-JIT0.698× [0.693, 0.703]0.658× [0.652, 0.662]0.675×28 / 30
Normal 13, interpreters, capture fcfb1934
RowZipp interpreterQuickJSQuickJS-NGNode jitlessDeno jitlessBun no-JITZipp/QuickJS
async-promise-chain331.63,9893,898456.8447.7561.50.083
class-prototype-hot3,2923,8377,2411,7191,7865,0080.858
json-large462.8786.21,428532.4552.3645.90.589
map-set-heavy575.41,5322,164819.41,2291,1070.376
markdown-render571.41,0742,066766.7796.1972.20.532
parse-large-js1,3212,0413,6711,3881,3881,5880.647
polymorphic-objects663.0860.91,411596.0561.31,3080.770
regex-log-scan1,4522,5334,0051,5461,5382,7710.573
sparse-array257.0386.7782.0207.9227.0275.50.665
typedarray-math3,8143,8486,5282,2622,3833,2070.991
polymorphic-objects-v2298.1399.1850.6300.3325.3419.10.747
property-ic-shapes1,9842,0275,1871,9551,9781,8100.979
sparse-array-v2278.91,843874.6324.7306.2484.10.151
Hostile 17, interpreters, capture fcfb1934
RowZipp interpreterQuickJSQuickJS-NGNode jitlessDeno jitlessBun no-JITZipp/QuickJS
calls-baseline85.7111.7251.2104.3120.0107.10.767
calls-closures198.6227.6451.5177.9195.9247.10.873
shapes-stable78.3132.4235.588.5104.389.40.592
shapes-megamorphic121.1194.1328.0115.8130.5185.70.624
types-stable131.5163.3374.7130.0148.3131.70.805
types-churn79.996.3202.297.8111.2114.00.830
branch-control91.4109.6236.1105.0118.3102.90.835
throw-catch81.997.6293.3164.4144.6153.10.840
allocation-ephemeral101.8250.2365.4120.0142.5260.20.407
allocation-survival98.3174.2269.4105.4123.4132.40.564
async-burst27.9103.2125.160.778.543.20.271
async-lived28.4107.7126.949.869.665.20.263
reactish-reconcile65.6119.4177.474.091.8105.60.549
warm-router90.1121.4474.178.897.9142.90.742
bytecode-vm290.7411.1946.5301.5315.8486.30.707
module-hot-graph95.295.7195.3103.3122.0145.50.995
npm-nanoid437.4699.01,168453.6471.6813.10.626

Over all 30 rows Zipp's interpreter, with no native code (ZIPP_NOJIT=1 ZIPP_NO_RX_JIT=1), is faster than Bellard's QuickJS (0.574×, 30 of 30 rows; 0.683× in the previous capture), and faster than QuickJS-NG on all of them (0.326×). QuickJS wins no row. Against the JIT engines with their JITs off it is faster than Node --jitless (0.911×, 21 of 30 rows; 1.105× in the previous capture), faster than Deno --jitless (0.825×, 24 of 30 rows; 0.995× in the previous capture), and faster than Bun without its JIT (0.675×, 28 of 30 rows; 0.804× in the previous capture).

Python versus CPython

Zipp compiles Python 3 to the same register bytecode. tools/python_bench.py runs 36 programs (30 micro kernels, 6 application-shaped) as a fresh process per engine per repetition, interleaved, 15 repetitions, against CPython 3.13.14; every run's output must equal CPython's. The binary is the published 0.0.31 release build (the PGO build has no Python). Each program times its own workload in-process; that work time is the ratio below. Zipp runs with its JIT and again with ZIPP_NOJIT=1, since standard CPython is an interpreter. 8 further programs (the torch ones) have no CPython counterpart and are not included.

Zipp / CPython, geometric means of the work-time ratios
SetZipp with JITZipp, no JIT
Micro 300.916×1.384×
Macro 61.591×1.630×
All 361.004×1.422×
All, whole-process wall time0.494×0.538×
Python programs, work-time medians in milliseconds, CPython 3.13.14
ProgramSetZippZipp no JITCPythonZipp/CPythonNo JIT/CPython
micro/attr_read_writemicro3.0310.397.870.3851.330
micro/builtin_isinstancemicro9.8215.409.391.0451.643
micro/builtin_lenmicro8.1013.6111.470.7071.184
micro/call_defaultsmicro17.3416.5012.431.3951.324
micro/call_keywordsmicro9.999.377.991.2501.185
micro/call_simplemicro39.9738.0336.421.0971.040
micro/dict_getmicro6.8012.2611.260.6041.094
micro/dict_str_keymicro33.2941.3420.421.6302.040
micro/float_arithmicro2.3410.599.560.2451.119
micro/for_listmicro6.1423.7716.660.3681.425
micro/fstringmicro6.588.133.471.8972.349
micro/generator_itermicro37.3441.5714.552.5662.852
micro/global_readmicro21.5336.2150.670.4250.715
micro/hasattr_missmicro2.884.732.781.0381.708
micro/hash_strmicro7.389.737.910.9321.239
micro/instance_newmicro13.3615.395.762.3192.663
micro/int_arithmicro57.0470.28146.450.3900.480
micro/list_appendmicro4.8611.035.110.9512.155
micro/list_indexmicro15.3125.8526.330.5810.979
micro/listcomp_smallmicro12.0213.857.001.7181.969
micro/math_sqrtmicro11.7916.909.811.2021.724
micro/method_callmicro14.4114.028.461.7031.633
micro/property_readmicro12.2313.189.101.3441.452
micro/range_loopmicro19.5552.26100.330.1950.525
micro/str_long_len_indexmicro4.786.846.860.6980.996
micro/str_methodsmicro22.0323.177.482.9463.108
micro/super_callmicro14.7914.238.321.7781.712
micro/try_raise_catchmicro22.1324.338.872.4942.731
micro/tuple_swapmicro0.751.672.910.2580.570
micro/tuple_unpackmicro6.2710.829.110.6881.186
macro/fib_recursivemacro38.8838.9022.081.7611.758
macro/json_roundtripmacro6.726.707.810.8600.856
macro/nbodymacro12.4217.039.081.3671.877
macro/oo_simulationmacro23.5622.7612.731.8511.789
macro/sort_heapqmacro12.0911.235.842.0721.928
macro/word_countmacro20.3819.209.992.0401.926

With its JIT, Zipp's Python is faster than CPython on 15 of 36 programs; over the micro kernels it takes 0.916× CPython's time, over the application-shaped programs 1.591×, and over all 36 programs 1.004× (1.067× in the previous capture); without the JIT Zipp is 1.422×. Counting the whole process, Zipp's faster start makes it 0.494× CPython's wall time. No intervals are computed for these geometric means; the JIT and no-JIT artifacts hold every observation.

WebAssembly 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.

The Python frontend's measurements are in Python versus CPython above. There are no GPU performance numbers at all; the GPU work is validated for correctness only.

The 11 September capture, and why it is slower

An earlier 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. 1.90× Node, headline ten

    First attributable default capture; interpreter-only smoke at 6.42×.

  2. PGO adopted at −13.3%

    1.749× to 1.451× in one build change; two more waves reach 1.330× the same day.

  3. Nursery on by default: 1.212×
  4. First capture under 1.20×

    typedarray-math reaches Node parity.

  5. 0.9695×: Node parity crossed

    Wave 19 decomposed the three rows that were 60% of the remaining gap.

  6. Hostile suite under parity at 0.961×
  7. A 2.16× regression, reversed

    A de-fused call lowering; root-caused and reverted the same day.

  8. 0.878× headline, 0.728× all-30: the public capture
  9. 1.003× headline after strict call order

    Correctness kept; speed to be recovered.

  10. 0.795× headline, 0.703× all-30: the previous capture

    All 13 normal rows faster than Node; seven hostile gaps remain.

  11. 0.769× headline, 0.634× all-30: the 0.0.26 capture

    All 13 normal rows faster than Node; 6 hostile gaps remain, and the bytecode VM is no longer one.

  12. 0.766× headline, 0.652× all-30: the 0.0.28 capture

    All 13 normal rows and 10 of 17 hostile rows faster than Node; the interpreter at 0.685× Bellard's QuickJS (0.683× rerun with its regex JIT off too).

  13. 0.744× headline, 0.629× all-30: the 0.0.31 capture

    All 13 normal rows and 12 of 17 hostile rows faster than Node; the interpreter at 0.574× Bellard's QuickJS.

A release measurement for v0.0.31

The published binaries are built by the release recipe: profile-guided since 0.0.31, but with the release's features (Python, GPU and Wasmtime) and their own training run, so each release is also measured as downloaded. These are descriptive release checks, separate from the canonical capture above. For v0.0.31 (the published zipp-0.0.31-windows-x86_64-portable.exe, source fcfb1934, 29 October 2026; Node 24.19.0, Bun 1.3.14 and Deno 2.6.10, 15 counterbalanced repetitions, paired medians with bootstrap intervals, exact output on every row):

v0.0.31 release build against Node 24, cold process medians
RowNode 24ZippZipp / Node
async-promise-chain331 ms233 ms0.70×
class-prototype-hot299 ms226 ms0.75×
json-large256 ms214 ms0.83×
map-set-heavy641 ms456 ms0.71×
markdown-render265 ms176 ms0.66×
parse-large-js268 ms224 ms0.83×
polymorphic-objects328 ms207 ms0.63×
polymorphic-objects-v282 ms23 ms0.29×
property-ic-shapes258 ms11 ms0.04×
regex-log-scan462 ms401 ms0.87×
sparse-array79 ms74 ms0.93×
sparse-array-v2171 ms96 ms0.56×
typedarray-math200 ms140 ms0.70×

The geometric mean is 0.757× Node over the ten headline rows [0.753, 0.759] and 0.549× over all 13 [0.546, 0.553]; Zipp's median is below Node's on every row. A paired A/B of the same 13 rows puts the profile-guided build at 0.996× the release build's time [0.991, 1.003] (the two differ in features and build flags as well as in their profiles): the download is within 1% of the canonical figures. The release and A/B artifacts hold the observations.

Reproduce it

From the repository root, PowerShell, x64 developer prompt
& '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