Repository navigation
ZIPP 0.0.32
The release that carries 0.0.31's TypeScript, Node programs and WebAssembly,
with its first fixes. zipp app.ts runs TypeScript and TSX directly, with no
transpile step; zipp app.js runs a program the way node does, with
Node's globals, npm packages and the core modules most packages reach for;
and WebAssembly is there, compiled to machine code by Wasmtime in the
64-bit command lines.
New in 0.0.32: WebAssembly in a large program runs on Wasmtime, a
JavaScript object handed to WebAssembly is let go once WebAssembly can no
longer reach it, and the WebAssembly modules hand a host its app's state as
one JSON text instead of thousands of calls across the boundary.
Reflect.set and Object.defineProperty are faster, and an arrow function
in a class method sees the method's locals over the class's name.
Coming from 0.0.30 or earlier? Everything 0.0.31 brought is in this release
too, and is described below after what is new.
New in 0.0.32
- WebAssembly in a large program runs on Wasmtime. 0.0.31 chose a VM's
WebAssembly runtime by whether its JIT was on, and a JavaScript program of
256 functions or more starts with its JIT off for its size, as does one
compiled lazily: every real script fell to the wasmi interpreter. A counting
loop took 42 ms in a small file and 321 ms once the file also held 300
unrelated functions. Only the JIT turned off (ZIPP_NOJIT=1, an embedder
disabling it, a trace being recorded) sends WebAssembly to wasmi now. - An object handed to WebAssembly is let go. Wasmtime's runtime kept
every JavaScript value WebAssembly was ever given as anexternreffor the
VM's life. A module passing objects by reference, as wasm-bindgen's do, kept
all of them: an app built that way grew from 3.6 GB to 9.1 GB in two
minutes in zipp-browser, and now holds steady at about 2.3 GB. A value is
now released once WebAssembly can no longer reach it, and still comes back
as itself while it can. (wasmi has no collector for its references, and
keeps them as before.) - A host reads an app's state in one crossing. The WebAssembly modules'
getGlobalsBatchbuilt what it returned property by property across the
boundary: for each property a descriptor object, each of its field names
and the key made on the host's side, and the definition, about eleven calls
a property. It now writes a batch that JSON can carry exactly as one JSON
text, charged against the conversion budget exactly as before, and the host
parses it: each property an own data property, as before,__proto__
included. A batch holdingundefined, a number JSON cannot spell, a lone
surrogate or aFloat32Arrayconverts as it did. Everywhere else a
property defined on a host object takes two calls instead of eleven. Reading
the state of a 400-object simulation takes 1.4 ms in Node instead of 8.3,
and about 2 ms in ZIPP instead of 94. Reflect.setandObject.definePropertywithout a descriptor read
back. A descriptor whose fields are data properties of ordinary objects
is read in place, without naming each field as a new string, and
Reflect.setcreates a property as a class field's definition does. Code
shaped like wasm-bindgen's glue (each call through itshandleError
wrapper) runs in 820 ms instead of 1,298; creating 320,000 properties with
Reflect.set, 75 ms instead of 267;Object.definePropertywith a literal
descriptor, 47 ms instead of 84.WebAssembly's members are writable data properties, as in a browser,
so a page or library can wrapWebAssembly.instantiate(see Changes in
behaviour).- An arrow function in a class method sees the method's locals over the
class's own name; minified code that named both alike stopped working in
0.0.31 (see Fixes).
Also in this release, from 0.0.31
- TypeScript and TSX run as they are. The JavaScript parser reads a
.ts,.mts,.ctsor.tsxfile with its types and drops them;
there is no transpile step and no JavaScript text, and the compiler, the
interpreter and the JIT run it as they run JavaScript. Constructs with a
run-time meaning are lowered as tsc emits them:enum(constant-folded
and reverse-mapped; aconst enuminlined where read),namespace,
constructor parameter properties,import x = …andexport = ….
Imports used only as types are elided,tsconfig.jsondecides class
fields (useDefineForClassFields) and legacy decorators
(experimentalDecorators, withemitDecoratorMetadata), and modules
resolve as TypeScript names them. JSX (.tsx, and.jsxwithout types)
is lowered to the calls tsc emits for the project'sjsxsetting, React's
automatic runtime by default. A.tsfile runs as a script unless it
does not parse as one. - Node programs.
zipp app.js,app.mjs,app.tsandapp.tsxrun
withprocess,Buffer,URLandURLSearchParams, timers,
EventTarget,AbortController,structuredClone, CommonJS (require,
module,exports,__filename,__dirname), npm packages resolved
fromnode_modulesbyexports,mainand Node's probing, and the core
modulespath,events,util,fs(synchronous, callback and promise
forms, and streams),os,tty,url,module,assert,buffer,
crypto(hashes and HMAC for md5, sha1 and sha256; not a cryptographic
random source),string_decoder,timers,streamandreadline.
Output is written as the program prints.
docs/NODE.md
lists what is and is not there;ZIPP_NO_NODE=1gives the plain
ECMAScript global object. - WebAssembly.
WebAssembly.validate,compile,instantiateand
their streaming forms,Module(withimports,exportsand
customSections),Instance,Memory,TableandGlobal, with
i64as BigInt andexternrefas the JavaScript value itself. Memory is
shared, not copied: a memory'sbufferis over its own bytes. In the
Windows x86-64, Linux x86-64 and Linux arm64 command lines Wasmtime
compiles modules to machine code unless the JIT is turned off; the wasmi
interpreter runs them with the JIT turned off (ZIPP_NOJIT=1, or
ZIPP_WASM_INTERP=1for WebAssembly alone) and in the armv7 build.
A loop of 200 million WebAssembly iterations takes 36 ms on Wasmtime and
289 ms on wasmi (Node 24: 73 ms), measured on 0.0.31. - WebAssembly is held to the limits a script runs under. Under a step
budget or a heap limit (zipp sandbox's, or an embedder's
set_limits/set_heap_limit), every WebAssembly instruction is charged
to the budget, on either runtime, and every memory and table counts
against the heap limit before it is allocated. A host's abort request
stops WebAssembly within milliseconds: Wasmtime checks at each 5 ms
epoch, wasmi between slices of its fuel (except inside a module's start
function, which wasmi cannot pause; there the budget bounds it).
zipp sandbox's child runs WebAssembly on wasmi, as it runs JavaScript
with the JIT off. - A faster JIT. Among the changes:
- a class method called on an instance (
a.dot(b)) is inlined at the
call site behind the class's guard,thisbound to the receiver, and
its property reads take the shape its own call site saw; - a class method's or getter's call from compiled code is a native call
through a per-site lookup, and an own function property's call too; - arithmetic that runs on doubles compiles to double operations kept in
registers,Math.sqrt,abs,floor,ceilandtruncare one
instruction each, and an Int product stays an Int; - a property read or written at the shape its site last saw is a direct
load or store, and a verified receiver's values stay at hand through a
straight run of code; - a loop that no compiled region serves enters the compiled function at
its head, andtry/finallyjumps compile; JSON.parsebuilds objects from shared key plans,JSON.stringify
decides a shape's keys once, and short decimals print without the
general formatter;/re/.test(x)makes no object, two different string literals compare
unequal without reading their bytes, and an object literal's spread of
a literal defines its properties in place.
- a class method called on an instance (
- Profile-guided command lines. The Windows x86-64, Linux x86-64 and
Linux arm64 command lines are built with profile-guided optimization, as
they have been since 0.0.31 (7ee15ed3): on each runner, an instrumented
build of the release feature set runs the canonical training corpus, and
the release is built with that profile. The armv7 command line and the
WebAssembly modules stay plain builds. - A sixth WebAssembly module,
javascript-typescript, the JavaScript
engine with the TypeScript front end, beside the JavaScript-only one. The
sandbox loader (zipp.mjs1.5.0) runs a TypeScript program in it as it
runs a JavaScript one, with the same bridges, host calls andfetch;
every other engine refuses TypeScript, naming this one.
Downloads
| File | What it is |
|---|---|
zipp-0.0.32-windows-x86_64-portable.exe |
The Windows command line; run it from anywhere. |
zipp-0.0.32-windows-x86_64-installer.msi |
The same binary, installed for the current user into %LOCALAPPDATA%\Programs\ZIPP and added to PATH; no administrator rights. Uninstall from Settings > Apps. |
zipp-0.0.32-linux-x86_64 |
The Linux command line, x86-64 (glibc 2.35 or later). chmod +x it after downloading. |
zipp-0.0.32-linux-arm64 |
The same for 64-bit Arm. |
zipp-0.0.32-linux-armv7 |
The same for 32-bit Arm (hard float); interpreter only, and WebAssembly on wasmi. |
zipp-0.0.32-javascript.wasm |
The JavaScript engine for browsers, Node, Deno and Bun. |
zipp-0.0.32-javascript-typescript.wasm |
The same with TypeScript and TSX. |
zipp-0.0.32-javascript-lite.wasm |
The JavaScript core without Intl and Temporal, built for size. |
zipp-0.0.32-javascript-python.wasm |
JavaScript and Python, torch built in. |
zipp-0.0.32-javascript-python-base.wasm |
JavaScript and Python without torch. |
zipp-0.0.32-torch-package.wasm |
The torch package for the -python-base engine. |
zipp-0.0.32-<engine>.wasm.br |
Each WebAssembly module compressed with Brotli (quality 11), about a quarter of its size. Serve it with Content-Encoding: br and Content-Type: application/wasm, or let zipp.mjs load it; the plain .wasm is the default. |
zipp-0.0.32-wasm-embed-kit.zip |
zipp.mjs (the loader and sandbox, version 1.5.0, unchanged) and its types, the host SDK (1.4.0), the engine's TypeScript declarations, the browser GPU adapter, docs and three examples. |
SHA256SUMS |
Checksums of every file. |
Changes in behaviour
New in 0.0.32:
WebAssembly's members are writable data properties, as in a browser:
writable, configurable and not enumerable. Each was an accessor without a
setter, soWebAssembly.instantiate = wrapperdid nothing, without an
error, and a page or library wrapping it went on calling the original.
The members become data properties when the first of them is read or
assigned; before that,Object.getOwnPropertyDescriptorstill describes
the accessor.
From 0.0.31, for anyone coming from 0.0.30 or earlier:
zipp file.jsruns in Node mode (and.mjs,.ts,.tsx): the
globals above are defined. A bundle that sniffsprocessorrequireto
pick its platform now takes its Node path, as it would undernode; run
it withZIPP_NO_NODE=1for the plain ECMAScript environment. A program
that uses none of the layer pays about half a millisecond at startup.WebAssemblyis a global in the command line and native embeddings
(the browser modules and the hardenedzipp-sandboxbuild have none).- A script under a step budget or heap limit that made WebAssembly
before the budget was set cannot run that WebAssembly under it: its
store counts no steps, so a call fails with aRangeError.
Fixes
New in 0.0.32:
- An arrow function in a class method saw the class's name instead of a
local of the method with the same name: inside
class e { m() { let e = {}; return () => e } }the arrow returned the
class. Minified code names both alike; softn.com's UI parser found its
document root to be its own class and no softn app would open.
From 0.0.31:
- A
finallyin an array builtin's callback was skipped under the JIT:
[1, 2, 3].map((x) => { try { return x * 2; } finally { n++; } })left
nat 0 in 0.0.30. A builtin now runs a callback withtry,catchor
finallyon its own frame.
Dependencies
No dependency changed since 0.0.31. Those 0.0.31 brought:
- wasmi 2.0.0 (with wasmi_core 2.0.0) runs WebAssembly in every native
build; Wasmtime 36.0.19, its long-term support line, with Cranelift, and
wasmparser 0.236 for custom sections, compile it in the 64-bit command
lines. Wasmtime had been at 44.0.3 while the WebAssembly work was on the
development branch, a version with eight RustSec advisories (two
critical: a component-model stack buffer overflow, and a wrongbr_table
jump target on arm64); 36.0.19 has the fixes for all eight.
Performance
0.0.32's published binaries have not been benchmarked against 0.0.31's.
What follows is what was measured while 0.0.32 was prepared, and then the
comparison of the published 0.0.31 and 0.0.30 binaries from the 0.0.31
notes, which describes the engine this release builds on.
Measured while 0.0.32 was prepared
On Windows x86-64 (Ryzen 9 9950X3D), with release builds of the engine
without link-time optimization:
- The
Reflect.setandObject.definePropertyfigures above are medians of
three runs of one program, 160,000 descriptors or 320,000 properties a run,
under the JIT (the program is small). - The state read above is
getGlobalsBatchover 400 objects of ten
properties, 300 of three and one of three, the mean of 200 reads after 20
to warm up, in Node 24.19.0 and in the ZIPP command line running the
Node-target module. - In zipp-browser, softn.com's predator-prey app read its state 6.9 times a
second instead of 5.7 with the engine changes alone, on the module softn.com
serves; the module change reaches an app when it ships a 0.0.32 module.
The 0.0.31 release comparison
Measured on 29 October 2026 on an otherwise idle machine, native Windows
x86-64 (Ryzen 9 9950X3D):
- the published 0.0.30 and 0.0.31 Windows binaries;
- a profile-guided build of the 0.0.31 source by the canonical recipe
(tools/pgo.sh, JavaScript only), which the release does not ship; - Bellard's QuickJS 2026-06-04 and Node 24.19.0.
Every ZIPP binary runs with address-space randomization turned off, so
image placement does not move the numbers. Each run is pinned to one core
of the processor's die with the 96 MB L3 cache unless stated otherwise.
The figures are medians of interleaved runs.
Bundled applications. Each library is bundled into one script that runs
a fixed workload. Each figure is the median of 7 runs of zipp js app.js,
and the time includes compiling the bundle. 0.0.31 runs them with
ZIPP_NO_NODE=1, as plain ECMAScript, which is what they were bundled
for: in Node mode the TypeScript bundle takes its Node path, which its
bundled os module cannot serve, and fails as it does under Node. The
0.0.30 and 0.0.31 columns have the code cache off, as on a first run. The
warm column has the cache filled.
| Program | 0.0.30 | 0.0.31 | 0.0.31, cache warm | Bellard's QuickJS | Node 24 |
|---|---|---|---|---|---|
Babel: @babel/standalone transforms about 20 KB of modern JSX twice |
3.50 s | 3.05 s | 2.93 s | 3.68 s | 0.77 s |
TypeScript: transpileModule on about 40 KB of classes, generics and enums |
0.93 s | 0.88 s | 0.68 s | 1.05 s | fails: its bundled os is a stub |
| Prettier: formats JavaScript, TypeScript and Markdown | 0.46 s | 0.41 s | 341 ms | 0.44 s | 358 ms |
| marked, lodash, acorn, terser, three.js, immer, RxJS and date-fns | 0.71 s | 0.66 s | 0.61 s | 1.91 s | 347 ms |
| Vue server rendering | 85 ms | 79 ms | 67 ms | 85 ms | 73 ms |
| Preact server rendering | 75 ms | 62 ms | 59 ms | 73 ms | 63 ms |
| React server rendering | 69 ms | 61 ms | 56 ms | fails | 63 ms |
On the die with the 32 MB L3 cache, 0.0.31 takes 0.83x to 0.97x
0.0.30's time, and 0.87x to 0.98x QuickJS's on Babel, TypeScript,
Prettier, Vue and Preact (0.83x to 0.93x on the larger-cache die).
The profile-guided build takes 0.83x to 0.91x QuickJS's time on those
five on the larger-cache die and 0.87x to 0.97x on the other.
Node 24, whose optimizing JIT compiles these long-running bundles' hot
code, is faster than 0.0.31 on Babel (3.94x), the eight libraries
(1.91x), Prettier (1.14x) and Vue server rendering (1.08x), given as
0.0.31's time over Node's; 0.0.31 is ahead on React server rendering
(0.96x) and about even on Preact server rendering (0.99x).
0.0.31 was the first release whose command lines are built with
profile-guided optimization, and on the bundles that is most of its lead
over 0.0.30. A plain release build of the same engine source, without the
profile, takes 1.07x to 1.16x the published binary's time on the bundles,
and over the 78 programs 1.055x interpreted and 1.066x with the JIT (same
machine and die, runs of their own). Taken with the comparison above, the
engine changes alone come to about 0.96x to 1.01x 0.0.30's time on the
bundles, which is to say about even, and about 0.96x interpreted and 0.95x
with the JIT over the 78 programs. TypeScript, Node programs and
WebAssembly are new, not faster.
Peak working set and page faults, cache off (mean of 3 runs):
| Program | 0.0.30 | 0.0.31 | Bellard's QuickJS |
|---|---|---|---|
| TypeScript | 225 MiB, 57,853 faults | 234 MiB, 60,180 | 114 MiB, 29,986 |
| Babel | 245 MiB, 64,590 | 251 MiB, 67,715 | 96 MiB, 25,433 |
| The eight libraries | 209 MiB, 53,669 | 212 MiB, 54,350 | 32 MiB, 9,433 |
| Prettier | 126 MiB, 32,512 | 131 MiB, 33,755 | 38 MiB, 11,164 |
| Vue server rendering | 37 MiB, 9,672 | 43 MiB, 10,998 | 20 MiB, 5,248 |
| Preact server rendering | 32 MiB, 8,484 | 34 MiB, 8,990 | 14 MiB, 3,786 |
0.0.31 holds 2 to 9 MiB more than 0.0.30 on these programs. A one-line
program's peak is unchanged (7.5 MiB, 7.6 with ZIPP_NO_NODE=1; 9.4 MiB in
Node mode, which sets up Node's globals), so the difference is in the work,
not the larger binary.
The 78 benchmark programs (median of 3 runs each; every output
identical):
- Interpreted (
ZIPP_NOJIT=1, which leaves the regex JIT on, as in
0.0.30's notes): 0.912x 0.0.30's time, faster on 59 of 78. The largest
gains arenew_class(0.58x),new_obj(0.61x),accessors-proxy
(0.74x),warm-router(0.77x),async-promise-chain(0.79x); the
largest losses (medians of 3 runs, not rerun) aredv_get(1.15x),
f64_write(1.12x),i32_rw(1.09x),dv_get_glob(1.08x). - With the JIT: 0.892x 0.0.30's time, faster on 68. The largest gains
arenew_class(0.51x),new_obj(0.55x),call_method(0.56x),
finally-labels(0.67x),polymorphic-objects(0.71x). - Against Bellard's QuickJS in the same runs, in that lane: the
interpreter takes 0.742x QuickJS's time and is faster on 62 of the 78
programs. 0.0.30 took 0.815x and was faster on 54. The largest gaps left
arearguments_len(1.63x),f64_write(1.43x),upval_rw(1.32x),
proto_read(1.30x),i32_rw(1.26x). - Against Node 24: with the JIT, 0.0.31 takes 0.857x Node's time and is
faster on 48 of the 78 (0.0.30: 0.961x, 43); interpreted, against
node --jitless, 1.071x (0.0.30: 1.175x).
The TypeScript benchmarks (bench/typescript, five programs). Run as
TypeScript, zipp file.ts against Node 24 (best of 5): collections
1.89x Node's time, the expression compiler 2.74x, n-body 0.84x,
the raytracer 1.79x and records 1.11x. As the same programs in plain
JavaScript, since 0.0.30 cannot read TypeScript: interpreted, 0.0.31 takes
0.669x 0.0.30's time and 0.675x QuickJS's; with the JIT, 0.313x 0.0.30's
time and 1.425x Node's (0.0.30: 4.556x).
WebAssembly. A loop of 200 million iterations in one exported function
takes 36 ms on Wasmtime (the JIT on), 289 ms on wasmi (ZIPP_WASM_INTERP=1)
and 73 ms in Node 24, each the median of 5 runs, timed inside the
program around the call alone (the module is compiled before the clock
starts). In 0.0.31 that held only in a small program; from 0.0.32 a large
one gets Wasmtime too (see above).
Measured while 0.0.31 was prepared (Linux, the same release build with each
change switched on and off), a class method called on an instance takes
6.8 ns a call instead of 14.5 (the same body written out in the loop: 6.0
ns), and the raytracer benchmark 3.7% fewer cycles from that alone.
The figures in the README and
docs/PERFORMANCE.md
describe the profile-guided 0.0.31 capture of 29 October 2026;
docs/PERFORMANCE.md also lists the 0.0.31 release measurements.
Publishing
As for earlier releases, the engines the site serves are refreshed AFTER this
release exists, in one commit that:
- sets
zippEngineReleaseinlanding/package.jsonto0.0.32; - refreshes
landing/public/playground-runtimeandlanding/public/wasmfrom
zipp-0.0.32-javascript-python.wasmandzipp-0.0.32-javascript.wasm
(node crates/zipp-wasm/embed-kit/package.mjs extract, then
sync-playground.mjs --refresh-engine), SHA256SUMS-verified; - syncs the sandbox playground (
node landing/scripts/sync-sandbox.mjswith
the release's files) and rewrites the site's Brotli copies
(npm run compress:engines); - updates the version, size and SHA-256 that
landing/src/App.tsxand
landing/README.mdstate about the bundled engine.
Full Changelog: v0.0.31...v0.0.32