Skip to content

BLAKE3 through WASM ​

Source
demos/blake3-wasm
Files
The crate · Loading in a browser · The worker · JavaScript baseline · Browser tests

Where does WebAssembly pay off? This hashes with BLAKE3 from Rust, on Node.js and in browsers, blocking or in a worker, and measures it against the best JavaScript to find out.

A worked example, not a package: nothing here is published.

PackageWhat it isREADME
rustThe blake3 crate, built for WASM with SIMDThe crate and its build
typescriptThe same API on both platforms, with a worker, its tests in real browsers, and the benchHow to use it, and how it loads

What was measured ​

pnpm bench in typescript runs this on Node.js 24, Chromium, Firefox and WebKit, each with three implementations of the same function:

  • noble: @noble/hashes, the JavaScript library you would reach for. It's written for auditability, not speed.
  • tight: the fastest JavaScript I could write, with every step inlined and all state in local variables. It would be unpleasant to maintain, and it exists as the fair baseline. Without it, this demo would credit WASM with what is mostly noble's choice to be readable.
  • wasm: this demo.

Warm, time per call:

BytesNode.js: nobletightwasmChromium: tightwasmFirefox: tightwasmWebKit: tightwasm
641.6 µs0.2 µs0.3 µs0.2 µs0.4 µs0.2 µs0.4 µs0.2 µs0.3 µs
1 Ki16.1 µs1.5 µs2.2 µs1.6 µs2.2 µs2.3 µs2.0 µs1.8 µs2.0 µs
3 Ki49.6 µs4.4 µs6.3 µs4.5 µs6.5 µs7.1 µs5.9 µs5.4 µs5.8 µs
4 Ki68.0 µs5.8 µs2.5 µs6.0 µs2.6 µs9.6 µs2.6 µs7.3 µs4.2 µs
64 Ki1.04 ms94 µs36 µs95 µs36 µs154 µs36 µs118 µs65 µs
1 Mi16.8 ms1.50 ms0.56 ms1.51 ms0.55 ms2.44 ms0.57 ms1.91 ms1.00 ms
16 Mi273 ms24.2 ms9.1 ms24.3 ms8.9 ms39.0 ms9.0 ms31.0 ms15.5 ms

From nothing loaded to a first result, in a fresh process or page:

BytesNode.js: tightwasmChromium: tightwasmFirefox: tightwasmWebKit: tightwasm
641.0 ms5.9 ms2.3 ms2.7 ms5 ms2 ms3 ms3 ms
16 Ki2.5 ms5.6 ms5.7 ms2.6 ms5 ms2 ms5 ms3 ms
64 Ki5.4 ms6.0 ms8.8 ms2.6 ms8 ms3 ms8 ms4 ms
1 Mi12.1 ms6.7 ms11.8 ms3.8 ms36 ms4 ms15 ms5 ms
16 Mi35.4 ms16.9 ms35.4 ms14.0 ms74 ms21 ms55 ms27 ms

What was learned ​

  • WASM pays off within microseconds here, not milliseconds. Warm, it passes the best JavaScript at 4 KiB, about 6 µs of work, and stays about 2x to 4x ahead. Against noble it wins at every size measured.
  • The crossover is a step, not a slope. BLAKE3 hashes 1 KiB chunks, and the crate's SIMD only runs on 4 at once. At 3 KiB JavaScript is usually ahead, and at 4 KiB WASM is well ahead. Without SIMD, WASM was 1.5x faster than tight JavaScript in a one-off Node.js run, where SIMD is 2.7x.
  • Cold is where WASM wins most. A first call runs in JavaScript's slowest tier, while WASM is at full speed from the start. In browsers, from nothing loaded, WASM is ahead by 16 KiB at most. On Node.js it takes until somewhere between 64 KiB and 1 MiB, as importing this package there takes about 5 ms.
  • How long the work takes isn't what decides it. This repo's constant-consistent-hash through WASM stays 1.6x slower than TypeScript, even at 30 ms of work. Both are 32-bit integer math a JIT does well. What decides it is whether the work is something JavaScript can't do well: SIMD here, or 64-bit math.
  • Most of the gap to a library may not be WASM at all. Tight JavaScript is 11x faster than noble on Node.js and Chromium's figure is higher still, though only 1.8x on WebKit. If a hot path is slow, a better JavaScript implementation may be enough.

When to use a worker ​

A call into WASM blocks its thread, as any JavaScript function does. It's a question of how long a thread can be held, not of WASM: a 60 FPS frame is 16 ms, which WASM reaches at about 28 MiB here, and tight JavaScript at about 11 MiB.

Measured with Comlink at 16 MiB:

Node.jsChromiumFirefoxWebKit
Start a worker, through a first answer18 ms8 ms11 ms4 ms
Round trip, copying the bytes11.4 ms14.8 ms14 ms18 ms
Round trip, transferring the bytes9.0 ms9.3 ms10 ms17 ms

An empty round trip took 64 µs on Node.js, and was under the browsers' clock resolution. So keep a worker running rather than start one per call, and only use one for what would otherwise block too long.

What this leaves out ​

  • One machine: arm64 under Linux, in a container. WebKit is Playwright's Linux build, not Safari. Firefox's clock only resolves to 1 ms, so its small cold numbers are rough.
  • Network: pages are served from memory, so a cold browser number has no download in it. The .wasm is 37 KB (13 KB gzipped), for a browser to fetch as well.
  • Bundle size against noble, or memory.
  • Other work: this is one hash, chosen because it should favor WASM. It says nothing about work that doesn't use SIMD or 64-bit math.

It has no external services, so what it's tested against is real browsers.