Skip to content

ZIPP v0.0.30

Latest

Choose a tag to compare

@github-actions github-actions released this 27 Oct 02:00
· 3 commits to main since this release

ZIPP 0.0.30

A release for large programs on the interpreter. A bundled application of
256 functions or more now compiles each function body when it is first
called, so the parts of a bundle a run never enters are never compiled.
Its syntax tree takes about half the memory and every instruction half the
bytes, and the interpreter's property, call and collection paths are faster
on the code bundlers emit. Bundled applications take 0.77x to 0.95x
0.0.29's time, and the interpreter runs the 78 benchmark programs in 0.92x.

  • A large program compiles a function's body at its first call. A
    script of 256 functions or more, which zipp js runs in the interpreter,
    leaves every body that can wait to its function's first call. TypeScript's
    compiler bundle defers 10,082 bodies and a run compiles 2,219 of them;
    Babel's defers 5,278 and compiles 1,966. A deferred body is compiled in
    the context the main compile saved, with the variables it captures
    resolved then, so it behaves as an eager compile would.
    ZIPP_LAZY=off compiles every program whole.
  • Less memory for code. An instruction is 16 bytes, not 32, and a heap
    slot 64 bytes, not 80. A statement in the syntax tree is 80 bytes (176),
    an expression 48 (56), and lists are allocated at their length.
    Identifiers are interned once per spelling. After the parse, Prettier's
    syntax tree is 19.3 MB instead of 34.9, TypeScript's 46.5 instead of
    100.6, Babel's 26.7 instead of about 55. TypeScript's peak working set
    falls from 318 to 225 MiB.
  • The interpreter runs bundled code faster. Among the changes:
    • an own field read or written through many object layouts is answered
      from the receiver's shape and a (shape, key) memo, without the general
      property path;
    • f.apply(this, arguments), f.bind(o), Array.prototype.values.call(a)
      and a native applied to a list run without the general method call;
    • arr.pop(), a private method call, a Map or Set method read into a
      variable, x == null, === against undefined, null or a boolean,
      and a run of integer switch cases each take one dispatch or one table
      lookup;
    • an absent field read through a class instance or through many AST
      layouts is cached;
    • a Proxy handler's trap and a built-in prototype's own method are read
      where they lie.
  • Collections cost less on large heaps. A minor collection roots only
    the prototypes and literals stored since the last one, a major runs after
    256 minors rather than 64, and the adaptive nursery stops at 64K
    allocations so it fits a 32 MB cache. The per-slot tables grow in a few
    large steps, so Preact server rendering takes 8,490 page faults instead
    of 10,259.
  • String.prototype.normalize follows Unicode 18.0, and a DataView's
    byteLength and byteOffset are 0 once its buffer is detached or
    shrinks beneath it, as the latest Test262 expects.

Downloads

File What it is
zipp-0.0.30-windows-x86_64-portable.exe The Windows command line; run it from anywhere.
zipp-0.0.30-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.30-linux-x86_64 The Linux command line, x86-64 (glibc 2.35 or later). chmod +x it after downloading.
zipp-0.0.30-linux-arm64 The same for 64-bit Arm.
zipp-0.0.30-linux-armv7 The same for 32-bit Arm (hard float); interpreter only.
zipp-0.0.30-javascript.wasm The JavaScript engine for browsers, Node, Deno and Bun.
zipp-0.0.30-javascript-lite.wasm The JavaScript core without Intl and Temporal, built for size.
zipp-0.0.30-javascript-python.wasm JavaScript and Python, torch built in.
zipp-0.0.30-javascript-python-base.wasm JavaScript and Python without torch.
zipp-0.0.30-torch-package.wasm The torch package for the -python-base engine.
zipp-0.0.30-<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.30-wasm-embed-kit.zip zipp.mjs (the loader and sandbox, version 1.4.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

  • String.prototype.normalize follows Unicode 18.0 (17.0 in 0.0.29).
  • A DataView's byteLength and byteOffset are 0 when its buffer is
    detached or has shrunk beneath it
    , as a typed array's are. They threw
    a TypeError. This is tc39/ecma262#3972, which has consensus; the
    Test262 pin moves to tc39/test262 0b4e0812 (26 October 2026), whose
    tests expect it. Node still throws.
  • The sandbox loader (zipp.mjs 1.4.0) checks its options more
    strictly.
    • It reads only an options object's own properties, so a polluted
      Object.prototype can neither grant a permission nor turn off Worker
      isolation.
    • isolate must be a boolean; quotas, size limits and
      limits.instructions must be integers.
    • Seeded storage items and file entries count against their quotas, and
      a sandbox whose seeds exceed them does not start.
    • A trusted storage backend owns its data and limits, so it cannot be
      combined with initial or quota, and its prefix must be a
      nonempty string. Sandboxes sharing a backend and prefix share its
      state; give each tenant its own prefix.
    • A network allowlist entry must be an http(s) URL without credentials.
      Only a bare origin (https://api.example.com) grants its whole origin;
      any other entry is a URL prefix, so https://api.example.com/?scope=read
      no longer allows https://api.example.com/other.
    • Under isolation, a host function's return value must survive the
      Worker's structured clone. One that does not answers its call with
      { ok: false, error: { code: "host" } }.
  • A large script's function bodies wait for their first call (above).
    A syntax error anywhere in the script, a regular expression literal's
    included, is still reported before it runs.
  • zipp js's code cache reads and prunes only folders it made. A
    folder under ZIPP_CACHE_DIR that the cache did not create is never read
    or deleted: a cache folder holds a marker, and pruning removes a folder
    only when every file in it is a cache entry, never through a symbolic
    link or junction.

Fixes

  • A self-recursive function with more than 255 parameters passed the
    wrong arguments to itself with the JIT
    : the call's argument count was
    carried in eight bits. Such a call no longer enters the tier that
    truncated it.
  • f.apply(t, a), Reflect.apply, Reflect.construct, f(...a) and
    [...a] read an array element redefined as an accessor from its old
    value.
    Object.defineProperty(a, 1, { get }) left the old value in
    the array's dense store, and those paths copied the store. They now run
    the getter, as Node does, and follow a getter that grows or shrinks the
    array.
  • pop(), shift() and splice() on an array whose length is
    non-writable threw before touching it.
    The specification removes or
    moves the elements first and then fails the write of length, as Node
    does; they now do the same. push(), unshift() and frozen arrays still
    throw at once.
  • A primitive's property read consulted Object.prototype past a
    prototype set to null
    : after
    Object.setPrototypeOf(String.prototype, null), "x".hasOwnProperty
    was still the method. It is undefined, as in Node.
  • zipp sandbox (and zipp js --sandbox) took an error reading the
    child's output for its end
    , so a failed pipe could pass for a
    complete, shorter result. It now reports the error.
  • Re-arming a heap limit after a conviction did not always collect. A
    host that re-armed its limit (set_heap_limit) after a conviction, then
    again once the script had dropped what filled the heap, got no
    collection from the second re-arm, so the next allocations could be
    convicted at once. A re-arm now collects a heap that is over its new
    ceiling, and a conviction that leaves the heap past half the ceiling has
    the next re-arm collect too.
  • The browser engine's accelerator bridge had no reply limit. A reply
    from setAccelBridge's compile is now held to the 32 MiB the other
    synchronous host bridges allow, counted in UTF-8 bytes.

Dependencies

  • unicode-normalization 0.1.26 (Unicode 18.0), which the engine's
    normalization tables are generated from and checked against.
  • For the site: vite 8.3.5, @cloudflare/vite-plugin 1.65.0 and wrangler
    4.152.0. Miniflare's undici is held at 7.30.1, which fixes
    GHSA-4qv8-2p9j-2685, GHSA-f72v-6w45-q472 and GHSA-xjj6-f33q-929c; the
    Cloudflare tooling's own releases still pin 7.29.1.

Performance

Measured on 27 October 2026 on an otherwise idle machine, native Windows
x86-64 (Ryzen 9 9950X3D):

  • the published 0.0.29 and 0.0.30 Windows binaries;
  • Bellard's QuickJS 2026-06-04.

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
in its default mode, and the time includes compiling the bundle. The 0.0.29
and 0.0.30 columns have the code cache off, as on a first run. The warm
column has the cache filled.

Program 0.0.29 0.0.30 0.0.30, cache warm Bellard's QuickJS
Babel: @babel/standalone transforms about 20 KB of modern JSX twice 4.35 s 3.34 s 3.26 s 3.55 s
TypeScript: transpileModule on about 40 KB of classes, generics and enums 1.11 s 0.90 s 0.72 s 1.03 s
Prettier: formats JavaScript, TypeScript and Markdown 0.53 s 0.44 s 0.37 s 0.43 s
marked, lodash, acorn, terser, three.js, immer, RxJS and date-fns 0.80 s 0.68 s 0.63 s 1.87 s
Vue server rendering 92 ms 80 ms 69 ms 82 ms
Preact server rendering 74 ms 71 ms 70 ms 72 ms
React server rendering 74 ms 66 ms 61 ms fails (no TextEncoder)

Prettier is the one bundle where 0.0.30 still takes longer than QuickJS on
this die (1.02x). On the die with the 32 MB L3 cache, 0.0.30 takes 0.77x
(Babel) to 0.95x (Preact) 0.0.29's time; there it takes 1.03x QuickJS's
time on Babel, 1.04x on Prettier and 1.01x on Preact, 0.98x on Vue and
0.90x on TypeScript. A profile-guided build of the same engine
(tools/pgo.sh, which the release does not ship) takes 0.82x to 0.94x
QuickJS's time on Babel, TypeScript, Prettier, Vue and Preact on the
larger-cache die and 0.85x to 0.95x on the other.

Peak working set and page faults, cache off (mean of 3 runs):

Program 0.0.29 0.0.30 Bellard's QuickJS
TypeScript 318 MiB, 83,034 faults 225 MiB, 57,832 114 MiB, 29,986
Babel 240 MiB, 64,128 245 MiB, 64,567 96 MiB, 25,433
The eight libraries 218 MiB, 55,897 209 MiB, 53,711 32 MiB, 9,434
Prettier 159 MiB, 40,892 127 MiB, 32,591 38 MiB, 11,165
Vue server rendering 51 MiB, 13,121 37 MiB, 9,658 20 MiB, 5,248
Preact server rendering 39 MiB, 10,259 32 MiB, 8,490 15 MiB, 3,785

Babel holds slightly more because a major collection now runs after 256
minors rather than 64, which leaves garbage longer in exchange for fewer
traces of its large old heap.

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.29's notes): 0.917x 0.0.29's time, faster on 74 of 78; over 9 runs
    none of the rest is more than 2% slower. The largest gains are
    try/catch (0.73x), property reads (0.77x), a burst of promise
    reactions (0.78x), array reads (0.80x), arguments.length and a loop
    over global variables (0.81x each), zero-argument calls (0.82x) and the
    eight-way switch loop (0.83x).
  • With the JIT: 0.971x 0.0.29's time, faster on 57. Slower, each
    confirmed over 9 runs: a zero-argument call loop (1.07x), a survival
    workload and object literals (1.04x each), and closure calls, accessors
    and Proxy, and a branch-heavy loop (1.02x each).
  • Against Bellard's QuickJS in the same runs, in that lane: the
    interpreter takes 0.818x QuickJS's time and is faster on 54 of the 78
    programs. 0.0.29 took 0.892x and was faster on 43. The largest gaps left
    are constructing objects and class instances (1.7x), arguments.length
    (1.6x), closure variable writes (1.4x), accessors and Proxy, and
    inherited reads (1.3x each).

The figures in the README and
docs/PERFORMANCE.md
still describe the profile-guided 0.0.28 capture of 21 and 22 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.30;
  2. refreshes landing/public/playground-runtime and landing/public/wasm from
    zipp-0.0.30-javascript-python.wasm and zipp-0.0.30-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, which brings the site's zipp.mjs to loader 1.4.0)
    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.29...v0.0.30