JZ (javascript zero) is a distilled JS subset that compiles to fast, minimal WASM.
| Good for | Not for |
|---|---|
| DSP, audio, synthesis | UI, DOM, frontend state |
| Images, video, pixels | Network, hot I/O, serving HTTP |
| Simulation, physics, games | Dynamic object models and monkey-patching |
| Parsers, codecs, compression | Allocation-heavy, long-lived object graphs |
| Scientific, numeric, edge ML | Security-sensitive cryptography and arbitrary-precision integers |
| Hashing, checksums, RNG | Tiny calls where the JS/WASM boundary dominates |
site / try it / examples / benchmarks
npm install jzimport { compile } from 'jz'
const wasm = compile('export const dist = (x, y) => (x*x + y*y) ** 0.5')
const { instance } = await WebAssembly.instantiate(wasm)
instance.exports.dist(3, 4) // 5Options
Options are passed as jz(source, opts) or compile(source, opts):
| Option | Use |
|---|---|
modules: { specifier: source } |
Static ES imports to bundle. CLI import resolution does this from files automatically. |
imports: { mod: host } |
Host imports import { fn } from "mod". |
memory |
Pass memory: N for owned memory with N initial pages, or memory: jz.memory() / WebAssembly.Memory to share across modules. maxMemory: N caps growth; importMemory: true imports env.memory instead of exporting own. |
host: 'js' | 'wasi' |
Runtime-service lowering. Default js; wasi for standalone runtimes. |
optimize |
false/0 off, 1 minimal, true/2 default (all stable passes), 3/'speed' trades size for speed, 'size' for smallest wasm. (Object form for per-pass overrides is internal/unstable.) |
define |
Compile-time constants injected as top-level bindings, e.g. { DEBUG: false, PORT: 8080 } (numbers, booleans, strings, null, or literal arrays/objects). |
strict: true |
Skip jzify lowering and reject dynamic fallbacks such as obj[k], for-in, and unknown receiver methods. |
alloc: false |
Omit allocator exports (_alloc/_clear) from modules that never marshal heap values. |
noSimd: true |
Disable auto-vectorization. Explicit f32x4 and i32x4 intrinsics still compile. |
whyNotSimd: true |
Report the first operation that prevented each loop from being vectorized. Warnings go to the warnings sink. |
experimentalStencil: true |
Vectorize neighbour-load stencils such as b[i] = f(a[i-1], a[i], a[i+1]) and 2-D 5-point sweeps to f64x2. Unstable and off by default. |
experimentalOuterStrip: true |
Strip-mine a pixel loop containing an inner reduction into f64x2. Each lane keeps scalar accumulation order. Unstable and off by default. |
randomSeed |
Set a number for a reproducible Math.random sequence. The default uses host entropy; true requests entropy explicitly. |
wat: true |
compile() returns WAT text instead of WASM binary. |
names: true |
Emit a WASM name section (function symbols) for profilers/debuggers. |
profile |
Mutable sink for compile-stage timings (entries/totals per phase). |
npm install -g jz
jz program.js # → program.wasm
jz program.js --wat # → program.wat
jz program.js -o out.wasm # custom output (- for stdout)
jz program.js -O3 # optimization: -O0 off, -O1 minimal, -O2 default, -O3 speed (-Os for size)
jz program.js --host wasi # standalone WASI output
jz --strict program.js # pure canonical subset (also implied by .jz extension)
jz -e "1 + 2" # eval → 3jz --help
jz - min JS → WASM compiler
Usage:
jz <file.js> Compile JS to WASM (full JS subset; .jz = strict)
jz --strict <file.js> Strict mode: pure canonical subset, no lowering
jz --jzify <file.js> Transform JS → jz source (auto-derives output file)
jz -e <expression> Evaluate expression
jz --help Show this help
Examples:
jz program.js # → program.wasm
jz program.js --wat # → program.wat
jz program.js -o out.wasm # custom output name
jz program.js -o - # write to stdout
jz program.js -O3 # optimize for speed
jz program.js -Os # optimize for size
jz program.js -D DEBUG=false # inject a compile-time constant
jz program.js --memory 64 # 64 initial pages (4 MB)
jz program.js --host wasi # emit WASI Preview 1 imports
jz --strict program.js # strict mode
jz --jzify lib.js # → lib.jz
jz -e "1 + 2"
Options:
--output, -o <file> Output file (.wat, .wasm, or - for stdout)
-O<n>, --optimize <n> Optimization level: 0 off, 1 minimal, 2 default (all
stable passes), 3 speed. -Os optimizes for size.
--define, -D <K=V> Inject a compile-time constant (VALUE parsed as JSON,
else string). Repeatable.
--host <js|wasi> Runtime-service lowering (default js)
--memory <pages> Initial memory size in 64 KiB pages
--max-memory <pages> Cap memory growth at this many pages (default unbounded)
--import-memory Import env.memory instead of exporting own memory
--no-alloc Omit _alloc/_clear allocator exports (standalone wasm)
--no-simd Disable auto-vectorization (no v128) for non-SIMD engines
--why-not-simd Report, per loop, why the auto-vectorizer declined it
--experimental-stencil Vectorize neighbour-load stencils (a[i±1]); opt-in
--experimental-outer-strip Strip-mine pixel loops over an inner reduction to f64x2; opt-in
--no-tail-call Use ordinary call frames instead of return_call
--names Emit wasm name section for profilers/debuggers
--stats Print compile-phase timings to stderr
--strict Pure canonical subset: reject full-JS syntax + dynamic fallbacks
--jzify Transform JS to jz source (no compilation)
--eval, -e Evaluate expression or file
--wat Output WAT text instead of binary
--resolve Resolve bare specifiers via Node.js module resolution
--imports <file> JSON file with host import specs (e.g. {"env":{"fn":{"params":2}}})
--version, -v Show version number
chladni: swept-frequency nodal figures. |
dwa: local robot motion planning. |
hydrogen: electron probability clouds. |
See all examples.
What JS is supported?
┌────────────────────────────────────────────────────────────────────────┐
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ jz strict │ │
│ │ let/const arrows rest destructuring import/export │ │
│ │ if/else for/while/do-while/of break/continue │ │
│ │ try/catch/finally throw │ │
│ │ numbers strings booleans arrays objects template literals │ │
│ │ Math Number String Array Object JSON RegExp Symbol │ │
│ │ ArrayBuffer DataView typed arrays Map Set Atomics │ │
│ │ Float16Array base64/hex codecs TextEncoder timers Date │ │
│ │ crypto randomness URLSearchParams structuredClone Set algebra │ │
│ │ WASI file I/O │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ jz default (jzify) │
│ var function arguments switch │
│ class new this extends super private/static fields │
│ generators iterator helpers async/await Promise for await │
│ loose equality instanceof WeakMap WeakSet │
└────────────────────────────────────────────────────────────────────────┘
outside the model
eval Function with Proxy Reflect
property descriptors getters/setters live prototypes
dynamic import DOM Intl Temporal Node APIsWhat differs from JS?
- Numbers. Numbers are
f64. Proven integers usei32and wrap at ±2³¹. Applyingx | 0to an f64 with |x| ≥ 2⁶³ saturates instead of ES-wrapping. - Math. Basic operations are IEEE-exact. Transcendentals use JZ's own kernels
and may differ from the host library in their last bits.
Math.sumPreciseaccumulates exactly and rounds once. - Strings. Strings are UTF-8 bytes. Length, indexing, slicing, search, and regular-expression positions count bytes; case conversion is ASCII-only.
- Objects. Literal fields have fixed slots. Computed keys use hash storage; live prototype chains and property descriptors do not exist.
- Array indices. Indices coerce to
i32. Plain arrays are bounds checked; typed arrays use raw fixed-size linear-memory access, so invalid indices can read unrelated memory or trap. - Memory. There is no garbage collector. Call
memory.reset()between independent allocation batches. It invalidates every previous pointer. - Generators and async. Both lower to state machines. Jobs drain at host
boundaries;
tryacrossyieldorawaitis unsupported. - Dates. Date getters use UTC.
- BigInt. BigInt is signed 64-bit, not arbitrary precision.
The compiler accepts these differences to keep the language statically compilable and its emitted modules lean. Do not compile code whose correctness depends on full ECMAScript edge semantics.
What is not supported?
- Proxy and Reflect. Traps do not apply to structs with compile-time offsets.
- Property descriptors and accessors. Objects store values without
writable,enumerable, getter, or setter metadata. - Live prototype chains.
__proto__, delegation, and monkey-patching are unsupported.Object.create(proto)makes a shallow copy; method dispatch is static. - Deleting literal properties. Literal object shapes are fixed. Dictionary-mode
delete o[k]works. - eval, the
Functionconstructor, andwith. These would require a compiler or interpreter at runtime. - Intl and Temporal. ICU, CLDR, and timezone tables exceed the intended module size.
Dateuses UTC. - UTF-16 and Unicode tables. Strings are UTF-8 bytes. Unicode property classes, normalization, and locale case conversion are unsupported.
- Arbitrary-precision BigInt. BigInt is a signed 64-bit integer and wraps past its range. Security cryptography is outside the scope.
- Boolean identity in dynamic keys. Runtime boolean keys use their numeric carrier, so
o[b]reads'1'fortrue. Static boolean keys fold correctly. - WeakRef and FinalizationRegistry. There is no garbage collector to observe.
WeakMapandWeakSetuseMapandSetsemantics. - Legacy browser features, DOM, and Node APIs. ECMAScript Annex B specifies legacy compatibility features required in web browsers; JZ omits its additional syntax. DOM and Node services stay in the host.
Why no types?
Ordinary code already carries useful type evidence: let x = 0.5,
Float32Array, an array index, a loop counter. JZ infers it instead of
turning the file into another language. Ambiguous values take a slower,
always-correct dynamic path.
Can I use npm packages and ES modules?
Packages compile when their source fits the JZ language. Code using the DOM or Node APIs does not; host services must cross as imports.
- Relative imports (
./dep.js) bundle at compile time. - Bare package specifiers (
import { x } from "pkg") require the CLI's--resolveor source supplied via{ modules }. - Transitive imports work; circular imports fail at compile time.
- There is no runtime module resolution.
const { exports } = jz(
'import { add } from "./math.js"; export const f = (a, b) => add(a, b)',
{ modules: { './math.js': 'export const add = (a, b) => a + b' } }
)How do I call host functions?
Import from a named module in the compiled source, then provide that module
through { imports }. Functions become WASM imports; numeric constants fold.
jz(
'import { log } from "host"; export const f = x => { log(x); return x }',
{ imports: { host: { log: console.log } } }
)
jz(
'import { sin, PI } from "math"; export const f = () => sin(PI / 2)',
{ imports: { math: Math } }
)Can I interpolate values (template literals)?
As a tagged template, jz inserts interpolated values at compile time. Numbers and booleans inline directly; strings, arrays, and objects become JZ literals:
jz`export let f = () => ${'hello'}.length` // 5
jz`export let f = () => ${[10, 20, 30]}[1]` // 20
jz`export let f = () => ${{name: 'jz', count: 3}}.count` // 3
const scale = (x) => x * 10
jz`export let f = (n) => ${scale}(n) + 1` // f(2) → 21, host-calledInterpolated functions become host calls. Non-serializable values such as host objects and class instances use post-instantiation getters.
How do values cross between JS and WASM?
Numbers cross as f64 or i32. Heap values use tagged pointers internally; the
wrapped exports returned by jz() marshal arguments and decode results:
const { exports } = jz`
export const greet = s => s.length
export const dist = p => (p.x * p.x + p.y * p.y) ** 0.5
export const point = (x, y) => ({ x, y })
export const sum = a => { let n = 0; for (const x of a) n += x; return n }
`
exports.greet('hello') // 5
exports.dist({ x: 3, y: 4 }) // 5
exports.point(3, 4) // { x: 3, y: 4 }
exports.sum(new Float64Array([1, 2, 3])) // 6For raw instance.exports calls, memory.String, .Array, typed-array methods,
and .Object allocate on the WASM heap and return a pointer; memory.read
decodes a raw result. Object keys must match a compiled schema. Numeric arrays
of at most eight elements may return as WASM multi-values.
What ships at runtime, and where does it run?
The compiler runs at build time or synchronously in a browser/Worker. The resulting module runs in browsers, Workers, Node, Deno, Bun, and standalone WASM engines.
- Heap-free numeric modules ship as ordinary
.wasmwith no JZ runtime, memory, allocator, GC, or bundled JavaScript engine. - Heap values use the optional
jz/interopbridge, about 6 KB gzipped, for memory codecs, errors, WASI, and host imports. host: 'js'binds services such as time and console throughenv.*.host: 'wasi'emits WASI Preview 1 imports for standalone engines. When manually instantiating a reactor, callinstance.exports._initialize?.()once.
A module imports env, wasi_snapshot_preview1, or neither, according to what
the source actually uses.
How does memory work?
Heap-using modules use a growing bump allocator with no free list or garbage collector. Allocations are discarded in batches:
for (let i = 0; i < 1000; i++) {
const result = exports.process(100) // allocates on the WASM heap
memory.reset() // drop the whole batch
}memory.reset() invalidates every previous pointer. Scalar modules without heap
values omit the allocator.
For threads, sharedMemory: true compiles against shared WebAssembly.Memory,
with Atomics.* lowering to WASM thread operations; jz.pool(src, { threads })
runs one kernel across worker threads. Shared typed arrays and scalars cross;
strings and objects stay thread-local.
jz.memory() creates memory shared by multiple compiled modules. Schemas
accumulate, so one module can consume an object created by another:
const memory = jz.memory()
const a = jz('export const make = () => ({ x: 10, y: 20 })', { memory })
const b = jz('export const read = o => o.x + o.y', { memory })
b.exports.read(a.exports.make()) // 30Pass an existing WebAssembly.Memory to jz.memory(memory) to wrap it.
Is it fast?
JZ leads V8 and AssemblyScript by geometric mean on the covered corpus and targets near-native speed. The release gate is stricter than an average: JZ must be the fastest WASM on every case. Per-case numbers, missing target coverage, and every measured loss stay visible on the bench page; a rival win is a bug to close, not an exception to hide.
How small is the output?
A heap-free numeric program emits no memory, allocator, or startup function; an empty program emits an empty module. Runtime helpers and standard-library kernels are included only when reachable.
In the published benchmark corpus, size-optimized JZ modules are 1.02× the size of AssemblyScript modules by geometric mean. AssemblyScript's ports use unchecked array access while JZ retains JavaScript out-of-bounds guards. Most JZ modules in the corpus are single-digit kilobytes.
optimize: 'size'disables unrolling and SIMD.alloc: falseomits allocator exports from numeric modules.- Function names are omitted unless
names: trueis set.
The compiler stays in the build step; these sizes are what ships.
Which optimizations are applied?
At the default optimize: 2, JZ applies:
- Type and representation inference from syntax and use sites for parameters, results, objects, and arrays. Ambiguous values remain dynamic.
- Direct typed-array memory access with proven bounds and aliases.
- Escape analysis and arena rewind for short-lived aggregates.
- Constant folding, common-subexpression and dead-store elimination, inlining, invariant hoisting, induction reduction, and loop unrolling.
- SIMD-128 vectorization of independent maps, reductions, conditionals, and byte scans. Loop-carried dependencies remain scalar.
- Tree shaking and reachability-gated runtime helpers.
optimize: 3 / 'speed' accepts additional code size for throughput;
optimize: 'size' disables unrolling and SIMD.
How do I inspect or debug output?
- Run the same source under Node and compare results, allowing for the documented differences above.
jz program.js --watorcompile(src, { wat: true })prints WAT.- Search WAT for
v128to confirm vectorization and for__dyn_getor__ext_callto find dynamic fallbacks. --why-not-simdreports the first operation blocking each loop.--strictturns dynamic property and method fallbacks into compile errors.--namesemits symbols;--statsprints compile-stage timings.
Float loop counters, plain arrays, and loop-carried dependencies are common reasons a kernel remains slower or scalar.
How does JZ compare with Porffor, AssemblyScript, etc.?
- AssemblyScript emits lean WASM from a typed TypeScript-like language; its source is not directly executable JavaScript.
- Rust, C, Zig, Go, and MoonBit offer explicit static types and mature native toolchains, but require a second implementation when the source of truth is JS.
- Javy and ComponentizeJS accept broader JavaScript by shipping an interpreter or engine inside WASM.
- Porffor and scriptc target native executables rather than WASM. JZ emits WASM first and can then lower it to native C.
JZ keeps the source executable and testable as JavaScript, then gets speed by accepting a narrower, native-style semantic contract.
Can JZ compile itself?
Yes. npm run test:self compiles JZ's parser, jzify pass, compiler, optimizer,
and encoder into dist/jz.wasm. That WASM-hosted compiler then compiles real
programs, whose output is instantiated and checked against the native compiler.
dist/jz.wasm is a self-host test artifact, not a runtime shipped with compiled
programs.
Can JZ compile to native?
Indirectly. JZ emits WASM, which WABT's
wasm2c can translate to
C and a C compiler can turn into a native executable:
JS → JZ → WASM → wasm2c → C → clang → nativeThe native benchmark lane uses --host wasi -O3 --no-tail-call, then
wasm2c and clang -O3; it beats V8 on 19 of 21 watr examples and ties the
other two on the reference M4 Max run. A host harness and wasm2c runtime must be
linked, so this is currently a toolchain rather than a one-command JZ target.
See the native pipeline.
Is JZ production-ready?
JZ is experimental and pre-1.0. The supported language and WASM ABI may change; pin a version and re-test upgrades. CI runs the core suite, selected test262 language and built-in tests, benchmark checks, and a self-host build.
Adoption is ejectable: remove the JZ build step and the source remains JavaScript.