Headline

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

normal 13 0.545×, hostile 17 0.748×

Zipp/Bun, all 30 rows
0.496×

normal 0.458×, hostile 0.527×

Zipp/Deno, all 30 rows
0.434×

normal 0.473×, hostile 0.407×

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

7 rows are slower; they are listed below

Capture: 21 October 2026, the 0.0.28 engine as a clean profile-guided-optimization build recorded as zipp 0.0.28 from source 84012a2d (which differs from the release tag da0fe6b1 only in the website). 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.

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.28, SHA-256 dedff1a3…32aa83, source 84012a2d, 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
Zipp7.5
Node30.7
Bun50.9
Deno79.3

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 84012a2d, 21 October 2026
RowNodeBunDenoZippZipp/NodeZipp/BunZipp/Deno
async-promise-chain337.8375.8353.3258.10.768* [0.755, 0.780]0.6840.737
class-prototype-hot301.3338.6322.2233.60.778* [0.771, 0.791]0.6910.737
json-large261.9199.7316.4216.40.836* [0.791, 0.854]1.1000.695
map-set-heavy648.1757.11138.5461.70.710* [0.688, 0.772]0.6100.413
markdown-render267.6214.5302.3183.00.680* [0.666, 0.693]0.8290.615
parse-large-js269.9236.1282.1232.50.867* [0.849, 0.871]0.9930.832
polymorphic-objects327.6337.5335.3210.70.644* [0.640, 0.656]0.6260.632
polymorphic-objects-v281.294.7127.422.20.271* [0.263, 0.287]0.2310.172
property-ic-shapes258.0165.5305.59.30.037* [0.035, 0.037]0.0580.030
regex-log-scan479.5561.8457.4389.90.826* [0.790, 0.847]0.7010.866
sparse-array79.8106.9123.871.30.901* [0.889, 0.914]0.6750.586
sparse-array-v2171.4369.8181.493.60.547* [0.534, 0.553]0.2520.523
typedarray-math200.3916.4163.7139.30.693* [0.674, 0.700]0.1510.846
Aggregates versus Node
SetGeometric mean95% interval
Headline 100.766×[0.758, 0.771]
Diagnostics 30.176×[0.172, 0.180]
All 130.545×[0.540, 0.549]

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 84012a2d, Zipp/Node cold ratio
RowZipp/NodeRowZipp/Node
calls-baseline0.456calls-closures1.123
shapes-stable1.043shapes-megamorphic1.104
types-stable0.499types-churn0.693
branch-control0.839throw-catch0.272
allocation-ephemeral0.357allocation-survival1.377
async-burst0.574async-lived0.829
reactish-reconcile1.257warm-router1.253
bytecode-vm1.050module-hot-graph0.403
npm-nanoid0.992

Ordinary geometric mean 0.748× Node [0.740, 0.757], 0.527× Bun, 0.407× Deno; category-balanced 0.784× 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.257×, server 1.253×), and passes the row bound (the worst row, allocation-survival, is 1.377×).

Where Zipp loses

7 of thirty rows have a Node point gap, all of them in the hostile suite: allocation-survival 1.377×, reactish-reconcile 1.257×, warm-router 1.253×, calls-closures 1.123×, shapes-megamorphic 1.104×, bytecode-vm 1.050×, shapes-stable 1.043×. The pattern: long-lived allocation, property access across many or changing shapes, closure-heavy application code and a large switch dispatcher are where V8's optimizing tiers still win. Against the previous capture (18 October 2026, zipp 0.0.26), all 30 rows went from 0.634× to 0.652× Node; the row that improved most was throw-catch (0.368× to 0.272×), and the ones that moved most the other way were async-lived (0.723× to 0.829×) and allocation-ephemeral (0.314× to 0.357×). Zipp's own medians are 1.2% lower than in that capture, but Node's, the same v24.19.0 binary, are 4.4% lower (7.5% on the short hostile rows), so most of the ratios' movement is in the Node runs, not in Zipp.

Against Bun, Zipp loses json-large (1.100×). 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.615× [0.600, 0.621]0.740× [0.733, 0.747]0.683×25 / 30
QuickJS-NG0.390× [0.385, 0.396]0.387× [0.384, 0.391]0.388×30 / 30
Node jitless1.224× [1.213, 1.241]1.022× [1.015, 1.029]1.105×9 / 30
Deno jitless1.168× [1.155, 1.185]0.880× [0.863, 0.887]0.995×15 / 30
Bun no-JIT0.829× [0.812, 0.840]0.785× [0.773, 0.794]0.804×24 / 30
Normal 13, interpreters, capture 84012a2d
RowZipp interpreterQuickJSQuickJS-NGNode jitlessDeno jitlessBun no-JITZipp/QuickJS
async-promise-chain398.63,9253,862456.6453.4571.00.102
class-prototype-hot3,6923,8447,2431,7371,7264,9390.961
json-large541.0805.11,458545.6566.0662.50.672
map-set-heavy668.71,5742,118751.21,1471,1470.425
markdown-render686.61,0952,100775.5814.71,0120.627
parse-large-js1,7362,0383,6681,3711,3801,6090.852
polymorphic-objects852.5883.21,439602.5580.11,3700.965
regex-log-scan1,7082,5664,0681,5601,5182,8000.665
sparse-array290.0386.9808.2207.9219.8278.40.750
typedarray-math4,2383,8086,5222,2392,3683,2401.113
polymorphic-objects-v2390.1398.1855.6298.6319.1425.80.980
property-ic-shapes2,6782,0365,2611,9211,9691,8171.315
sparse-array-v2327.11,855876.8322.6314.2475.70.176
Hostile 17, interpreters, capture 84012a2d
RowZipp interpreterQuickJSQuickJS-NGNode jitlessDeno jitlessBun no-JITZipp/QuickJS
calls-baseline99.0110.7251.9103.0117.4106.80.894
calls-closures231.8233.6453.3175.7195.1246.30.992
shapes-stable87.2131.2261.186.6104.089.50.665
shapes-megamorphic156.3190.1328.7111.8131.1185.90.823
types-stable158.4163.0374.4126.1145.9129.80.971
types-churn105.397.4202.098.0110.9114.11.081
branch-control103.2109.4236.297.5116.3102.90.943
throw-catch99.997.9295.3160.9139.2150.91.020
allocation-ephemeral112.1253.2365.4120.6135.3259.60.443
allocation-survival106.1174.8264.1101.6122.2131.10.607
async-burst33.1101.9125.360.078.643.50.324
async-lived33.8107.8126.449.469.768.00.314
reactish-reconcile82.4123.5178.372.189.9106.40.667
warm-router119.1123.2474.176.5102.0140.50.967
bytecode-vm332.9404.6943.6298.5310.1484.00.823
module-hot-graph113.899.4194.7102.7121.5143.11.145
npm-nanoid514.8697.81,159456.3469.4800.60.738

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.683×, 25 of 30 rows; 0.685× on 21 October, when its regex JIT was left on), and faster than QuickJS-NG on all of them (0.388×). QuickJS wins property-ic-shapes (1.315×), module-hot-graph (1.145×), typedarray-math (1.113×), types-churn (1.081×), throw-catch (1.020×). Against the JIT engines with their JITs off it is slower than Node --jitless (1.105×, 9 of 30 rows), faster than Deno --jitless (0.995×, 15 of 30 rows), and faster than Bun without its JIT (0.804×, 24 of 30 rows).

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.28 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.975×1.400×
Macro 61.674×1.680×
All 361.067×1.443×
All, whole-process wall time0.539×0.576×
Python programs, work-time medians in milliseconds, CPython 3.13.14
ProgramSetZippZipp no JITCPythonZipp/CPythonNo JIT/CPython
micro/attr_read_writemicro3.3310.867.870.4231.375
micro/builtin_isinstancemicro9.8314.999.191.0701.638
micro/builtin_lenmicro8.3013.7711.510.7211.201
micro/call_defaultsmicro16.3615.7112.501.3091.266
micro/call_keywordsmicro9.258.977.961.1631.128
micro/call_simplemicro39.0537.8036.051.0831.039
micro/dict_getmicro6.1711.7111.220.5501.042
micro/dict_str_keymicro27.6835.9220.561.3461.772
micro/float_arithmicro2.3211.699.430.2461.244
micro/for_listmicro15.7423.2516.660.9451.400
micro/fstringmicro7.639.383.502.1832.693
micro/generator_itermicro43.9542.5314.453.0422.933
micro/global_readmicro22.1231.5650.220.4400.625
micro/hasattr_missmicro2.895.712.771.0452.058
micro/hash_strmicro6.929.187.890.8771.154
micro/instance_newmicro14.6816.835.722.5652.938
micro/int_arithmicro64.3077.65146.680.4380.533
micro/list_appendmicro4.7911.635.140.9322.267
micro/list_indexmicro17.6728.9426.350.6701.097
micro/listcomp_smallmicro13.5215.457.391.8302.059
micro/math_sqrtmicro10.8115.889.801.1021.612
micro/method_callmicro14.1014.038.501.6591.656
micro/property_readmicro12.2012.698.871.3761.429
micro/range_loopmicro19.9343.5999.250.2010.437
micro/str_long_len_indexmicro5.227.186.880.7591.045
micro/str_methodsmicro22.9524.747.223.1783.472
micro/super_callmicro15.2213.958.581.7741.645
micro/try_raise_catchmicro25.7028.199.062.8383.124
micro/tuple_swapmicro0.771.522.930.2620.520
micro/tuple_unpackmicro8.3311.259.090.9161.229
macro/fib_recursivemacro37.4736.2022.051.7001.642
macro/json_roundtripmacro6.976.977.840.8890.889
macro/nbodymacro14.3917.809.261.5541.896
macro/oo_simulationmacro25.1624.1712.611.9961.920
macro/sort_heapqmacro12.4611.765.982.0851.995
macro/word_countmacro22.3521.149.932.2512.119

With its JIT, Zipp's Python is faster than CPython on 15 of 36 programs; over the micro kernels it takes 0.975× CPython's time, over the application-shaped programs 1.674×, and over all 36 programs 1.067× (1.057× in the previous capture); without the JIT Zipp is 1.443×. Counting the whole process, Zipp's faster start makes it 0.539× 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).

A release measurement for v0.0.28

The published binaries are ordinary release builds, not profile-guided ones, so each release is also measured as downloaded. These are descriptive release checks, separate from the canonical capture above. For v0.0.28 (the published zipp-0.0.28-windows-x86_64-portable.exe, source da0fe6b1, 21 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.28 release build against Node 24, cold process medians
RowNode 24ZippZipp / Node
async-promise-chain328 ms299 ms0.91×
class-prototype-hot301 ms232 ms0.77×
json-large258 ms247 ms0.96×
map-set-heavy617 ms510 ms0.82×
markdown-render272 ms217 ms0.80×
parse-large-js268 ms244 ms0.90×
polymorphic-objects327 ms299 ms0.91×
polymorphic-objects-v281 ms26 ms0.32×
property-ic-shapes258 ms10 ms0.04×
regex-log-scan469 ms431 ms0.91×
sparse-array79 ms71 ms0.91×
sparse-array-v2171 ms99 ms0.58×
typedarray-math199 ms139 ms0.70×

The geometric mean is 0.856× Node over the ten headline rows [0.850, 0.869] and 0.608× over all 13 [0.605, 0.617]; 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.891× the release build's time [0.881, 0.901]: the download is about 12% slower than 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