← lab

WASM vs JS, measured three ways

One 7-rule scoring pass over synthetic payment records, implemented three times: objects with branches, flat typed arrays with the branches arithmetic-folded, and a 218-byte WebAssembly module written byte by byte — no Rust, no wasm-pack, no toolchain of any kind. All three run on your CPU when you press the button, and all three must return the identical flag count or the page refuses to show you a ratio.

Correction — 2026-08-07

Until today this URL was /lab/rust-wasm-fraud/ and it claimed to compare JavaScript against a “Rust / WASM” engine, reporting speedups around 10×. It shipped no WebAssembly. Line 3 of its bench.js read Simulates JS-vs-WASM scoring throughput, and the function labelled runWASM() was ordinary JavaScript over typed arrays. There was no .wasm file in the repository, so there was nothing to load and nothing to compile. The number in the “WASM speedup factor” box was real, but it measured JavaScript against other JavaScript under a label that said otherwise.

The old “WASM” column has not been deleted — it is column B below, under its true name, because it turns out to be the most interesting column on the page: most of what people attribute to WebAssembly is data layout, and column B is the data layout without the WebAssembly.

old URL /lab/rust-wasm-fraud/ → 301 → this page · the write-up at /blog/rust-wasm-browser-fraud/ carries its own correction and no longer claims a production engine · score.wasm (218 bytes) is served from this directory; check it in DevTools → Network → WASM.

reading environment…

Why there is no Rust here

There is no Rust toolchain on the machine that builds this site — no rustc, no cargo, no wat2wasm, no clang --target=wasm32. Adding one to fix a page about honesty would have meant the page shipped weeks later, or shipped with the old claim still on it. So the module is written the other way round: tools/gen-wasm.mjs emits the binary format directly — LEB128 integers, five sections (type, function, memory, export, code), one function body. Nothing generates it at request time. It runs once, on my machine, and the 218 bytes are committed to the repository like any other asset.

The generator refuses to write a file that WebAssembly.validate() rejects, then instantiates it and checks the flag count against the JavaScript reference over 200,000 seeded records. If those two numbers disagree, it exits non-zero and the deploy does not publish. That check runs in the same command as the Raft invariant gate.

What the honest ratio is

On this author's M-series laptop, in V8: B is about 2.7× faster than A, and C is about 2.0× faster than B — call it 5.5× end to end, with roughly three quarters of that coming from the data layout rather than from WebAssembly. Your numbers will differ and the page prints yours, not mine. What the old page claimed — 10×, attributed to WebAssembly — was not measured against any WebAssembly at all.

this workload is integer-and-f32 comparisons with no allocation and no calls; it is close to the best case for a JIT, which is exactly why the wasm margin is 2× and not 10×. workloads with dense f64 maths, manual memory, or SIMD show more. a workload that crosses the JS↔wasm boundary per record shows less than 1×.

The write-up, corrected → · Lab · Main site