Skip to content

ZIPP v0.0.31

Choose a tag to compare

@github-actions github-actions released this 29 Oct 09:03
· 220 commits to main since this release

ZIPP 0.0.31

A release for TypeScript, Node programs and WebAssembly. 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. The JIT is faster on
class-heavy code and on doubles.

  • 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 while the JIT is on; the wasmi
    interpreter runs them with the JIT off (ZIPP_NOJIT=1,
    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).
  • 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 for
    the first time (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.31-windows-x86_64-portable.exe The Windows command line; run it from anywhere.
zipp-0.0.31-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.31-linux-x86_64 The Linux command line, x86-64 (glibc 2.35 or later). chmod +x it after downloading.
zipp-0.0.31-linux-arm64 The same for 64-bit Arm.
zipp-0.0.31-linux-armv7 The same for 32-bit Arm (hard float); interpreter only, and WebAssembly on wasmi.
zipp-0.0.31-javascript.wasm The JavaScript engine for browsers, Node, Deno and Bun.
zipp-0.0.31-javascript-typescript.wasm The same with TypeScript and TSX.
zipp-0.0.31-javascript-lite.wasm The JavaScript core without Intl and Temporal, built for size.
zipp-0.0.31-javascript-python.wasm JavaScript and Python, torch built in.
zipp-0.0.31-javascript-python-base.wasm JavaScript and Python without torch.
zipp-0.0.31-torch-package.wasm The torch package for the -python-base engine.
zipp-0.0.31-<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.31-wasm-embed-kit.zip zipp.mjs (the loader and sandbox, version 1.5.0) 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

  • 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

  • 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

  • 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 since the WebAssembly work began 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

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 is the first release whose command lines are built with profile-guided
optimization (see the list above), 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).

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 these 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.31;
  2. refreshes landing/public/playground-runtime and landing/public/wasm from
    zipp-0.0.31-javascript-python.wasm and zipp-0.0.31-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.30...v0.0.31