Repository navigation
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, whichzipp jsruns 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=offcompiles 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, aMaporSetmethod read into a
variable,x == null,===againstundefined,nullor a boolean,
and a run of integerswitchcases 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.
- an own field read or written through many object layouts is answered
- 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.normalizefollows Unicode 18.0, and a DataView's
byteLengthandbyteOffsetare 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.normalizefollows Unicode 18.0 (17.0 in 0.0.29).- A DataView's
byteLengthandbyteOffsetare 0 when its buffer is
detached or has shrunk beneath it, as a typed array's are. They threw
aTypeError. This is tc39/ecma262#3972, which has consensus; the
Test262 pin moves to tc39/test2620b4e0812(26 October 2026), whose
tests expect it. Node still throws. - The sandbox loader (
zipp.mjs1.4.0) checks its options more
strictly.- It reads only an options object's own properties, so a polluted
Object.prototypecan neither grant a permission nor turn off Worker
isolation. isolatemust be a boolean; quotas, size limits and
limits.instructionsmust 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
backendowns its data and limits, so it cannot be
combined withinitialorquota, and itsprefixmust 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, sohttps://api.example.com/?scope=read
no longer allowshttps://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" } }.
- It reads only an options object's own properties, so a polluted
- 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 underZIPP_CACHE_DIRthat 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()andsplice()on an array whoselengthis
non-writable threw before touching it. The specification removes or
moves the elements first and then fails the write oflength, as Node
does; they now do the same.push(),unshift()and frozen arrays still
throw at once.- A primitive's property read consulted
Object.prototypepast a
prototype set tonull: after
Object.setPrototypeOf(String.prototype, null),"x".hasOwnProperty
was still the method. It isundefined, as in Node. zipp sandbox(andzipp 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
fromsetAccelBridge'scompileis 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'sundiciis 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.lengthand a loop
over global variables (0.81x each), zero-argument calls (0.82x) and the
eight-wayswitchloop (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:
- sets
zippEngineReleaseinlanding/package.jsonto0.0.30; - refreshes
landing/public/playground-runtimeandlanding/public/wasmfrom
zipp-0.0.30-javascript-python.wasmandzipp-0.0.30-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, which brings the site'szipp.mjsto loader 1.4.0)
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.29...v0.0.30