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…
A · objects, branches
One object per record, one function call per record, real
if / else if. What a normal codebase looks like.
B · flat typed arrays, JS
Same maths over one 12-byte-stride buffer, every
else if folded into two monotone predicates. This is the column
that used to be labelled “Rust / WASM”.
C · WebAssembly, 218 bytes
The identical loop, hand-emitted as wasm bytecode: 19 distinct opcodes, one function, one exported memory, zero imports.
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×.