Quality and methodology
- Applies to
- Compression, Allocation, Memory64, Build settings
- Sample
- A batch of at least 50 ms
- Reporting threshold
- ±3%
1. In brief
- Timing values are medians of samples timed in a Web Worker, each a batch of at least 50 ms; bytes and ratios are exact.
- Paired comparisons claim a difference beyond ±3% only when its 95% interval excludes no change; Compression compares unpaired medians.
- Headline claims on reference pages must hold in two independent captures; a run on this machine is shown without that check.
2. Timing, warm-up and order
Browsers coarsen performance.now(), so each sample times a batch of repetitions in a Web Worker and divides by its size. Calibration grows the batch (at most 1,000,000 repetitions) until it lasts at least 50 ms; builds compared in the same round share one count.
Untimed warm-up runs at least 2 rounds and 250 ms (500 ms for Memory64), at most 20 rounds. Each round visits everything once, in an order rotated each round, so slow drift spreads across all of them.
3. Paired ratios and the ±3% threshold
Two things timed in the same round give one ratio per round, so drift that hits both cancels. The effect is the median ratio (0.9 is “10% less time”); its 95% interval comes from order statistics, assuming independent rounds:
- 6 rounds: the 1st to the 6th smallest ratio (96.9% coverage).
- 10 rounds: the 2nd to the 9th smallest ratio (97.9% coverage).
- 15 rounds: the 4th to the 12th smallest ratio (96.5% coverage).
Below 6 rounds there is no interval and no claim.
An effect is claimed only when the median is more than 3% from 1 and the interval excludes 1. The ±3% band is a reporting threshold, set from the A/A control below; figures shade it as “not claimed”.
What the interval does not cover
It covers round-to-round sampling only, so a bias that stays with one module instance for a whole run is outside it. Build settings' A/A control, the baseline instantiated twice, shows such bias: in the 2026-11-08 edition, 13 of 88 Chrome and Firefox A/A rows excluded 1, all within about 2%; hence the 3% threshold. Safari's A/A rows show far larger bias on some workloads (cause not investigated); rows its A/A control rejects claim nothing.
4. What is paired, and what is not
Three pages pair their comparisons by round:
- Build settings: every variant, and the A/A instance, against the shipped build.
- Memory64: wasm64 against wasm32.
- Allocation: within a class only, talc against dlmalloc and bump-scope against bumpalo.
Compression compares many configurations at once, so it compares medians, unpaired: two are “about the same” when their medians are less than 3% apart, or when their observed min–max ranges overlap and neither is wider than ±10% of its median. Its bandwidth figures name a configuration fastest only where it beats the runner-up by more than 3%, with the same winner, in both captures.
Observed min–max is the fastest and slowest sample: it widens with more samples, while a confidence interval narrows.
5. Timer resolution and isolation
The site serves Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp, so every run is cross-origin isolated and gets the browser's finer clock. Each run measures the clock it got and the Measurement notes report it. With 50 ms batches, even a 1 ms step is at most 2% of one sample.
6. Reference editions
A reference edition is a dated capture in Chrome, Firefox and Safari on one stated machine, today one Apple M4 Pro (arm64); x86-64 is not measured. Safari has no memory64. Each capture records the machine, power state, browser version, isolation and timer, the method, the toolchain and library versions, the commit, a SHA-256 per wasm module and input, and every per-round sample as raw JSON.
Engines and machines are never pooled.
Every browser and input is captured twice, independently. The two must agree: their timing medians, matched series by series, may not differ by more than 1.25× overall, nor may more than 5% of series move by more than 1.5×. A pair that disagrees ran in a slowed state and is withheld everywhere, with a note.
7. This machine
“Measure on this machine” runs the same harness in your browser without a second capture. On a reference input it also reports this machine ÷ reference as a geometric mean; if your browser differs from the reference's, engine and machine are confounded.
Benchmarks run in your browser; files and results are not uploaded.
8. Per benchmark
Compression
- Speed is MB/s of uncompressed data (1 MB = 106 bytes), both ways; ratio is original ÷ compressed size. Levels follow each library's own scale.
- Every output is decoded and checked byte for byte before timing; a failure is marked and left off the figures.
- Each decoder decodes its own encoder's output, so the stream is part of its speed. In a separate test (Node 26, the M4; six decoders, five encoders' streams) the stream alone moved a decoder's speed by up to about 1.2× at level 6, so two streams can set two decoders up to 1.2² ≈ 1.44× apart. Decoders within 1.44× of a group's fastest are not ranked; one group is called faster only where its slowest decoder beats the next group's fastest by more than 1.44×, in both captures.
- The browser's own gzip runs through CompressionStream and DecompressionStream, Streams overhead included.
- Code size is kB after gzip level 6, measured at build time; JavaScript libraries use their published minified builds.
- Inputs are 4 of the Silesia corpus's 12 files, the ones zstd, lz4 and brotli publish results against: dickens (10,192,446 bytes), xml (5,345,280 bytes), x-ray (8,474,240 bytes), samba (21,606,400 bytes). Each is checked against a pinned SHA-256 when packed and by length when loaded; loading is never timed.
Allocation
The workload processes the 21 XML documents in the Silesia corpus's xml file (a tar) one at a time, largest first, so memory kept from a large document shows when smaller ones follow. For each document:
- Parse it into an owned tree, copying names, attribute values and text and growing child lists by push.
- Walk the tree, creating and freeing a path string per element, a normalised copy of each attribute value and a word list per text node. Fold everything into a checksum that every build must match.
- Keep a small survivor set (interned tag names and recent per-document summaries, trimmed now and then) in the global allocator, in all four builds.
- Drop the tree.
dlmalloc and talc serve everything as the global allocator. Each arena build keeps one arena for the module's lifetime; after every document it drops the tree and resets the arena. The arena's chunks come from dlmalloc.
All four builds run the same code on allocator-api2's collections; bump-scope discussion 108 explains why.
Timing and pairing. A sample times whole batches of every document, cleanup included (dropping each tree and, for arenas, the reset), divided by the number of documents. All four allocators run in every round, so a comparison within a class, talc against dlmalloc or bump-scope against bumpalo, is a paired ratio under the 3% claim rule. The cold first batch, which pays for memory.grow, is reported apart.
Heap pages. Linear memory grows in 64 KiB pages and never shrinks: pages are kept for the module instance's lifetime, counted over a fixed sequence on a fresh instance.
The allocator's share. An untimed counting pass records each build's allocator calls in one batch (allocations, grows, frees and bytes requested). Changing the allocator also changes memory layout, locality and copying, so the page compares whole-application times and does not split out an allocator's share.
Classes. dlmalloc and talc are general-purpose: each allocation is freed on its own and reused. bumpalo and bump-scope are arenas: everything is freed at once on reset, so memory only grows until then. The classes do different jobs, so allocators are compared only within a class.
Memory64
- One crate, a bevy_ecs editor session, built for wasm32 and wasm64 with the same nightly compiler and flags.
- Both builds run in every round, so wasm64 ÷ wasm32 time is paired. Every sample's checksum must match across builds.
- Memory differences within one 64 KiB page or 3% are not called.
Build settings
- The same crates built with SIMD off and on, each with no wasm-opt, -Os and -O3; the baseline is the shipped build, no SIMD with -Os.
- All variants and the A/A control run in every round, so any two compare as a paired ratio.
- Sizes are exact: raw, gzip and brotli bytes of each module, and its count of SIMD instructions.