src/blake3-fast.js is generated by tools/gen-fast.py, applying the techniques from
https://parsa.wtf/blake3/ that need code generation:
- all 7 rounds emitted straight-line, with the message permutation resolved at generation time, so the running code never permutes anything
- message words read through a
Uint32Arrayview, one load per word, instead of four byte loads and three shifts - the whole 16 block loop of a chunk inside one function, so nothing is passed and the chaining value stays in locals from a chunk's first block to its last
- no allocation per chunk: one reused scratch buffer for 4096 chunk CVs
Generated offline and committed, not new Function() at load time: runtime code
generation is exactly what would force unsafe-eval into the CSP, and staying out of the
CSP is one of pure JS's two advantages.
Firefox runs this code about 3x slower than Chromium. Eight shapes of the same algorithm were generated and measured to find out why, and the JIT was profiled. None closed the gap. The generator still emits them all, so the comparison can be re-run:
| shape | what changes | Firefox | Chromium |
|---|---|---|---|
chunkCVs (used) |
7 rounds unrolled, message words in locals | 249 MiB/s | 789 MiB/s |
chunkCVsB2 |
+ the 16 block loop unrolled by 2 | 249 | 798 |
chunkCVsB4 |
+ unrolled by 4 | 250 | 648 |
chunkCVsMem |
message words read from the array at each use | 249 | 763 |
chunkCVsX2 |
two chunks interleaved, to break the dependency chain | 229 | 790 |
chunkCVsLoop |
one round in a loop of 7, permutation by variable swaps | 198 | 298 |
chunkCVsSmall |
the rounds behind a per-block function call | 193 | 170 |
chunkCVsBlocks16 |
the whole chunk, 16 blocks x 7 rounds, straight-line | 4 | 4 |
tools/profile.py runs the Gecko profiler over a long single-threaded run and reports the
JIT tier of every frame. What it shows:
- The code is fully Ion compiled: 99.7% of samples in the working shapes are Ion frames, 0.3% baseline. So the gap is not a tiering, bailout or deoptimisation problem - Firefox is running optimised machine code and that code is simply ~3x slower.
chunkCVsBlocks16falls off a cliff into baseline: 99.1% of its samples are baseline frames, because the function is too large for the optimising JIT to take. Both engines do this, at 60x the cost. Any generated code has a size limit worth knowing about, and the shapes above sit under it.- No single operation explains it.
bench/micro.htmltimes the integer operations in isolation: Firefox is ~2x slower on dependent add and xor chains, ~1.4x on the rotate idiom - and faster than Chromium on a g() mix (654 vs 473 Mops/s). The 3x only appears in the full function.
Switching the typed arrays from Uint32Array to Int32Array, so no loaded word is ever
above 2^31 and has to become a double, was within noise. So the remaining difference is
in how Ion and TurboFan compile this particular shape of integer code at scale, and no
rearrangement of the JavaScript found a way round it.
Every row hashes 4 MiB, so latencies compare directly as well as rates. Run to run these vary by about 10%.
| 1 worker, 4 MiB | Firefox 155 | Chromium | Android WebView 151 |
|---|---|---|---|
| WebCrypto SHA-256 (the bar) | 5.1 ms - 781 MiB/s | 4.4 ms - 901 MiB/s | 11.7 ms - 427 MiB/s |
| WebCrypto SHA-512 | 11.2 ms - 357 MiB/s | 8.3 ms - 483 MiB/s | 9.8 ms - 510 MiB/s |
wasm, wasm32_simd |
2.1 ms - 1905 MiB/s | 2.2 ms - 1798 MiB/s | 2.8 ms - 1429 MiB/s |
| wasm, portable scalar | 7.0 ms - 573 MiB/s | 5.7 ms - 703 MiB/s | 5.7 ms - 702 MiB/s |
wasm, +simd128 flag only |
6.8 ms - 587 MiB/s | 5.9 ms - 677 MiB/s | 5.6 ms - 714 MiB/s |
| wasm kernel only, no copy | 6.9 ms - 580 MiB/s | 5.8 ms - 690 MiB/s | 5.5 ms - 727 MiB/s |
| wasm copy into linear memory alone | 0.1 ms | 0.1 ms | 0.1 ms |
| optimised JS | 16.0 ms - 249 MiB/s | 5.0 ms - 798 MiB/s | 6.2 ms - 645 MiB/s |
| readable JS | 30.1 ms - 133 MiB/s | 33.8 ms - 118 MiB/s | 43.7 ms - 92 MiB/s |
The Android numbers are an x86_64 emulator on this desktop, not a phone: an emulated Android runs the host's cores, so treat it as "the WebView engine on this hardware", which is what isolates the engine from the phone. A real device will be slower in proportion to its cpu, and the ratios are what carry over.
Two things carry over from it regardless. WebView is the one engine where the optimised JS beats the sha256 it would replace (645 vs 427 MiB/s, 1.5x), because Chromium's WebCrypto sha256 on this platform is half the speed of the desktop one while V8's JIT is not. And the wasm build is 3.3x sha256 there, the widest margin of the three.
At 5 MiB sha256 measures 814 MiB/s (Firefox) and 1033 (Chromium), so the rate hardly moves with input size.
| aggregate across workers, 4 MiB each | Firefox 155 | Chromium |
|---|---|---|
| wasm simd x2 / x4 / x16 | 2186 / 4051 / 8193 MiB/s | 3256 / 5626 / 9739 MiB/s |
| optimised JS x2 / x4 / x16 | 386 / 730 / 1182 MiB/s | 679 / 1281 / 2154 MiB/s |
| sha256 x2 / x4 / x16 | 1405 / 2602 / 3459 MiB/s | 1891 / 3061 / 5804 MiB/s |
Artefact sizes, which land in the initial page load: wasm 12 KB scalar, 14 KB with
+simd128, 29 KB with wasm32_simd; the generated JS is 1566 lines, ~60 KB
unminified.
wasm/— a small Rust crate wrapping the referenceblake3crate, built for wasm32-unknown-unknown, exposing the subtree entry points:subtree_cv(which isset_input_offset+update+finalize_non_root),merge_non_root,merge_root, plushash_allfor the vectors. Deliberately not wasm-bindgen: one exported memory and a preallocated input buffer, so the artefact is small, there is no generated JS glue, and the copy into linear memory stays visible to the benchmark.src/blake3-fast.js— generated bytools/gen-fast.py: the optimised pure JS candidate, fast only for the shape that matters here: whole, power-of-two, aligned runs of chunks. Do not edit it; edit the generator.src/blake3.js— the readable reference:hash(one-shot, whatb3sumprints) plus the same subtree API —subtreeCV,mergeNonRoot,mergeRoot. Written with the V8 techniques from https://parsa.wtf/blake3/ that cost nothing in readability: state in locals rather than arrays, straight-line rounds, permutation by renaming, no allocation per block. Not the generated-code or SIMD versions from that write-up.test/tests.mjs— the 35 official BLAKE3 vectors, the subtree-rebuild property at several sizes including the 4 MiB chunk shape, and rejection of misaligned subtrees.test/wasm.html— the same vectors through each wasm build, and every wasm subtree call and merge checked against the JS implementation.test/b3sum.mjs— the claim checked against the reference binary.bench/— the benchmark page, worker, wasm loader, and the built.wasmfiles.tools/browser.py— runs a page headless in Firefox (Marionette, no driver needed) or Chromium (chromedriver) and prints what it produced.tools/crosscheck.py— the b3sum comparison, driven through a browser.tools/fuzz.py— random data at random lengths through every implementation, with b3sum as the oracle for whole-input hashes and the readable reference as the oracle for subtree chaining values. Lengths are weighted towards block and chunk boundaries.tools/profile.py— runs the Gecko profiler over a long single-threaded run and reports the JIT tier of every frame.tools/android.py— runs a page in the real Android WebView. There is no marionette or chromedriver for a phone's WebView, so the page posts its results back to the server instead, reached overadb reverse.--buildbuilds and installs the tiny host app intools/wvbenchstraight from the SDK build tools: no gradle, no network.
python3 tools/browser.py firefox test # js tests, in a browser
python3 tools/browser.py firefox wasm # wasm vs vectors and vs the js impl
python3 tools/browser.py chromium bench # the numbers above
python3 tools/crosscheck.py firefox # vs the b3sum binary
python3 tools/fuzz.py 150 firefox # fuzz everything, random seed
python3 tools/fuzz.py 80 20260918 chromium # ... or a fixed one, as CI does
python3 tools/profile.py fast 8 # what the firefox jit did with it
# android webview, against a booted emulator or a usb device
$ANDROID_HOME/emulator/emulator -avd <avd> -no-window -gpu swiftshader_indirect &
python3 tools/android.py wasm --build # correctness, building the host app first
python3 tools/android.py bench # the numbers
node test/node.mjs # js tests, no browser
node test/b3sum.mjs # vs b3sum, no browser
# rebuilding the wasm (needs: rustup target add wasm32-unknown-unknown)
cd wasm && RUSTFLAGS="-C target-feature=+simd128" \
cargo build --release --target wasm32-unknown-unknown
cp target/wasm32-unknown-unknown/release/blake3_wasm.wasm ../bench/wasm/blake3-simdfeature.wasm- All 35 official BLAKE3 test vectors pass, in Firefox and Chromium, for the JS implementations and for all three wasm builds (153 assertions in total).
- Every wasm subtree CV and merge agrees with the JS implementation, at several chunk indices including a 4 MiB subtree at offset 4 MiB.
b3sumagreement on random files at 0, 1, 1023, 1024, 1025, 4 MiB−1, 4 MiB, 4 MiB+1, 8 MiB and 10 MiB bytes.- All 207 cross-implementation assertions pass in the Android WebView too.
- Fuzzing: 5677 checks over 168 MiB of random data at random lengths, in Firefox and Chromium, with zero failures - every shape, every wasm build, whole-input hashes against b3sum and subtree chaining values against the reference. The fuzzer was itself checked by injecting a single wrong xor into one shape, which it caught in 21 of 305 checks while reporting nothing against the others.
- The property the whole approach rests on: an 8 MiB file hashed as two 4 MiB subtree
chaining values and merged gives the same hash
b3sumprints. That is what makes the stored root hash the file's real BLAKE3 hash rather than a tree of one's own.