Skip to content

ZIPP v0.0.32

Latest

Choose a tag to compare

@github-actions github-actions released this 29 Oct 14:58
· 14 commits to main since this release

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 an externref for 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'
    getGlobalsBatch built 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 holding undefined, a number JSON cannot spell, a lone
    surrogate or a Float32Array converts 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.set and Object.defineProperty without 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.set creates a property as a class field's definition does. Code
    shaped like wasm-bindgen's glue (each call through its handleError
    wrapper) runs in 820 ms instead of 1,298; creating 320,000 properties with
    Reflect.set, 75 ms instead of 267; Object.defineProperty with 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 wrap WebAssembly.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, .cts or .tsx file 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; a const enum inlined where read), namespace,
    constructor parameter properties, import x = … and export = ….
    Imports used only as types are elided, tsconfig.json decides class
    fields (useDefineForClassFields) and legacy decorators
    (experimentalDecorators, with emitDecoratorMetadata), and modules
    resolve as TypeScript names them. JSX (.tsx, and .jsx without types)
    is lowered to the calls tsc emits for the project's jsx setting, React's
    automatic runtime by default. A .ts file runs as a script unless it
    does not parse as one.
  • Node programs. zipp app.js, app.mjs, app.ts and app.tsx run
    with process, Buffer, URL and URLSearchParams, timers,
    EventTarget, AbortController, structuredClone, CommonJS (require,
    module, exports, __filename, __dirname), npm packages resolved
    from node_modules by exports, main and Node's probing, and the core
    modules path, 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, stream and readline.
    Output is written as the program prints.
    docs/NODE.md
    lists what is and is not there; ZIPP_NO_NODE=1 gives the plain
    ECMAScript global object.
  • WebAssembly. WebAssembly.validate, compile, instantiate and
    their streaming forms, Module (with imports, exports and
    customSections), Instance, Memory, Table and Global, with
    i64 as BigInt and externref as the JavaScript value itself. Memory is
    shared, not copied: a memory's buffer is 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=1 for 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, this bound 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, ceil and trunc are 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, and try/finally jumps compile;
    • JSON.parse builds 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.
  • 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.mjs 1.5.0) runs a TypeScript program in it as it
    runs a JavaScript one, with the same bridges, host calls and fetch;
    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, so WebAssembly.instantiate = wrapper did 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.getOwnPropertyDescriptor still describes
    the accessor.

From 0.0.31, for anyone coming from 0.0.30 or earlier:

  • zipp file.js runs in Node mode (and .mjs, .ts, .tsx): the
    globals above are defined. A bundle that sniffs process or require to
    pick its platform now takes its Node path, as it would under node; run
    it with ZIPP_NO_NODE=1 for the plain ECMAScript environment. A program
    that uses none of the layer pays about half a millisecond at startup.
  • WebAssembly is a global in the command line and native embeddings
    (the browser modules and the hardened zipp-sandbox build 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 a RangeError.

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 finally in an array builtin's callback was skipped under the JIT:
    [1, 2, 3].map((x) => { try { return x * 2; } finally { n++; } }) left
    n at 0 in 0.0.30. A builtin now runs a callback with try, catch or
    finally on 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 wrong br_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.set and Object.defineProperty figures 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 getGlobalsBatch over 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 are new_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) are dv_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
    are new_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
    are arguments_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:

  1. sets zippEngineRelease in landing/package.json to 0.0.32;
  2. refreshes landing/public/playground-runtime and landing/public/wasm from
    zipp-0.0.32-javascript-python.wasm and zipp-0.0.32-javascript.wasm
    (node crates/zipp-wasm/embed-kit/package.mjs extract, then
    sync-playground.mjs --refresh-engine), SHA256SUMS-verified;
  3. syncs the sandbox playground (node landing/scripts/sync-sandbox.mjs with
    the release's files) and rewrites the site's Brotli copies
    (npm run compress:engines);
  4. updates the version, size and SHA-256 that landing/src/App.tsx and
    landing/README.md state about the bundled engine.

Full Changelog: v0.0.31...v0.0.32