Compression codecs in WebAssembly, Silesia corpus subset: November 2026
- Reference machine
- Apple M4 Pro (arm64) · macOS 26.5.2
- Samples
- 10 per configuration, 2 captures
1. Main points
- Of the levels tested, zstd 15 downloaded fastest or within 1% everywhere; clearly fastest from ≈100 to 250 Mbps except on x-ray (Figure 1).
- For uploads, zstd 9, zstd 3 then zstd 1 led as bandwidth rose, except for xml in Chrome, which starts with brotli 9 (Figure 2).
- In Firefox and Safari, the wasm Deflate codecs ran at 0.73–1.23× Chrome's speed; built-in gzip and the JavaScript libraries at 0.32–2.55× (Figure 3).
- libdeflate, zlib-rs and zune-inflate were among the fastest Deflate decoders wherever a group led clearly: 5 of 12 pairs (Figure 7, per input and browser).
2. Across inputs and browsers
Figure 1. Fastest download, by bandwidth
Download and decompress one file. Colour marks the fastest of the levels tested, by over 3% in both captures.
- zstd 15
- about the same (within 3%)
Arrow keys move between bandwidth bands; Up and Down move between inputs and browsers; Enter pins one so its values stay shown; Escape clears.
Figure 2. Fastest upload, by bandwidth
Compress and upload one file. Colour marks the fastest of the levels tested, by over 3% in both captures.
- zstd 9
- zstd 3
- zstd 1
- brotli 9
- about the same (within 3%)
Arrow keys move between bandwidth bands; Up and Down move between inputs and browsers; Enter pins one so its values stay shown; Escape clears.
Figure 3. Deflate speed relative to Chrome 156
Speed ÷ speed in Chrome 156, log scale; lines span 4 inputs, every level and capture.
- decompress
- compress
Arrow keys move between implementations; Left and Right between browsers; Enter pins one so its values stay shown; Escape clears.
Figure 4. Download size of compression and decompression code
kB after gzip level 6, measured at build time, by format. Browser gzip is built in; “same module” decompresses and compresses.
Arrow keys move between points; Enter pins one so its values stay shown; Escape clears.
Source: This build's files: the wasm modules as shipped, and pako's and fflate's published minified builds
Notes: pako has separate inflate and deflate builds; fflate is one bundle, so its decoder size includes its encoder.
3. Details for dickens in Chrome 156
Figure 5. zstd 15 gave the fastest download of the levels tested, at every bandwidth from 1 Mbps to 1 Gbps
Time to download, then decompress dickens, 10.2 MB, relative to sending it uncompressed; log scales.
- fastest by over 3% in both captures
- about the same
- other configurations
Arrow keys move between bandwidths; Enter pins one so its values stay shown; Escape clears.
| Bandwidth · fastest | Time per file (lower → upper bandwidth) | Uncompressed |
|---|---|---|
| 1 Mbps–1 Gbps · zstd 15 | 24 s → 33 ms | 82 s → 82 ms |
Notes: Payload only, codecs loaded; Browser gzip is DecompressionStream, not HTTP Content-Encoding. Slower devices move crossovers lower.
Figure 6. The fastest upload of the levels tested went from zstd 9 to zstd 1 as bandwidth rose
Time to compress, then upload dickens, 10.2 MB, relative to sending it uncompressed; log scales.
- fastest by over 3% in both captures
- about the same
- other configurations
Arrow keys move between bandwidths; Enter pins one so its values stay shown; Escape clears.
| Bandwidth · fastest | Time per file (lower → upper bandwidth) | Storage, $/mo per TB | vs previous winner | Uncompressed |
|---|---|---|---|---|
| below ≈2.5 Mbps · about the same: zstd 9, brotli 9, zstd 15 | 26 s → 11 s | $6.43 | — | 82 s → 33 s |
| ≈2.5–7 Mbps · zstd 9 | 11 s → 4 s | $6.90 | — | 33 s → 12 s |
| ≈7–20 Mbps · about the same: zstd 9, zstd 3 | 4 s → 1.5 s | $6.90 | about the same size | 12 s → 4.1 s |
| ≈20–150 Mbps · zstd 3 | 1.5 s → 240 ms | $7.70 | +12% | 4.1 s → 544 ms |
| ≈150–400 Mbps · about the same: zstd 3, zstd 1 | 240 ms → 111 ms | $7.70 | about the same size | 544 ms → 204 ms |
| above ≈400 Mbps · zstd 1 | 111 ms → 60 ms | $8.96 | +16% | 204 ms → 82 ms |
Notes: Storage counts only if the server keeps the client's bytes. Validating uploads, egress and recompressing: Measurement notes.
Figure 7. zune‑inflate, Browser gzip, zlib‑rs and libdeflate decoded at least 1.75× faster than the rest
Deflate decompression speed, MB/s on a log scale, dickens, level-6 streams; whiskers are the observed min–max.
Arrow keys move between points; Enter pins one so its values stay shown; Escape clears.
Notes: The encoder that wrote a decoder's input can alone set two decoders up to 1.44× apart. Each reads its own library's output unless noted.
| Implementation, level | Ratio | Compress | MB/s min–max | Decompress | MB/s min–max | Code, kB gzipped | Checksum on decode |
|---|---|---|---|---|---|---|---|
| Deflate | |||||||
| Browser gzipCompressionStream | 2.65× | 62.4 | 61.6–64.1 | 656.1 | 647–667 | built in | CRC-32 |
| miniz_oxide 1 | 1.90× | 149.9 | 149–152 | 250.9 | 247–258 | 25.5 both | none |
| miniz_oxide 6 | 2.63× | 31.1 | 30.6–31.6 | 349.0 | 343–356 | ||
| miniz_oxide 9 | 2.64× | 24.5 | 24.1–25.0 | 349.2 | 345–354 | ||
| zune-inflatedecodes miniz_oxide 6 output | 2.63׆ | — | no encoder | 693.9 | 686–701 | dec 14.0 · no encoder | none |
| libdeflate 1 | 2.42× | 204.7 | 203–207 | 592.9 | 571–605 | 35.6 both | CRC-32 |
| libdeflate 6 | 2.64× | 67.2 | 66.2–68.5 | 620.9 | 604–633 | ||
| libdeflate 9 | 2.67× | 33.5 | 33.0–33.9 | 601.1 | 594–612 | ||
| zlib-rs 1 | 1.69× | 200.6 | 197–204 | 506.7 | 506–509 | 56.4 both | none |
| zlib-rs 6 | 2.62× | 67.2 | 66.5–67.4 | 635.2 | 630–640 | ||
| zlib-rs 9 | 2.64× | 34.9 | 34.5–35.2 | 667.5 | 656–673 | ||
| pako 1JavaScript | 2.34× | 57.8 | 57.4–58.5 | 179.4 | 177–184 | dec 10.7 · enc 8.6* | Adler-32 |
| pako 6 | 2.65× | 27.0 | 26.7–27.3 | 198.6 | 192–203 | ||
| pako 9 | 2.65× | 24.0 | 21.6–24.2 | 199.2 | 197–203 | ||
| fflate 1JavaScript | 2.28× | 70.9 | 69.9–71.4 | 191.7 | 185–200 | 12.7 both* | CRC-32 not checked |
| fflate 6 | 2.58× | 41.0 | 40.5–41.4 | 222.6 | 209–231 | ||
| fflate 9 | 2.58× | 40.2 | 39.9–40.7 | 233.1 | 215–238 | ||
| Zstandard | |||||||
| zstd 1 | 2.39× | 400.9 | 398–407 | 1,243.3 | 1,230–1,260 | dec 51.2 · enc 121.8 | off by default |
| zstd 3 | 2.78× | 228.5 | 227–230 | 1,004.2 | 989–1,020 | ||
| zstd 9 | 3.11× | 35.8 | 34.0–36.1 | 1,132.7 | 1,110–1,160 | ||
| zstd 15 | 3.33× | 5.4 | 5.35–5.47 | 1,141.9 | 1,120–1,160 | ||
| LZ4 | |||||||
| lz4_flex | 1.58× | 356.2 | 351–362 | 2,205.3 | 2,190–2,220 | 26.5 both | off by default |
| Brotli | |||||||
| brotli 1 | 1.89× | 108.7 | 108–109 | 242.7 | 238–245 | dec 101.3 · enc 527.3 | none |
| brotli 4 | 2.70× | 69.4 | 69.2–70.0 | 451.7 | 445–456 | ||
| brotli 9 | 3.20× | 10.4 | 10.3–10.5 | 543.5 | 528–553 | ||
Measurement notes
- Conditions
- Chrome 156.0.8078.80 (headless) on macOS 26.5.2 (25F84); Apple M4 Pro, arm64, 14 logical cores; on AC power, Low Power Mode off. Captured 8 November 2026, 21:12–21:14 UTC. Capture 1 of 2.
- Clock
- Cross-origin isolated. Timer resolution in a worker 5 µs (median step; finest 5 µs).
- Method
- 10 samples per row, each a batch of at least 50 ms divided by its repetitions. Untimed warm-up of at least 250 ms and 2 rounds (at most 20). Order rotated by one each round.
- Codecs
- Each library's one-shot API at the levels shown, with its default window and settings; zstd with long-distance matching is off by default. Formats, as implementation (container): Deflate: Browser gzip (gzip), miniz_oxide (raw deflate), zune-inflate (raw deflate), libdeflate (gzip), zlib-rs (raw deflate), pako (zlib), fflate (gzip); Zstandard: zstd (frame); LZ4: lz4_flex (frame); Brotli: brotli (stream). Every decoded output was checked byte for byte against the input before timing.
- Throughput
- MB/s of uncompressed data in both directions; 1 MB = 10⁶ bytes. Ratio is original ÷ compressed size, exact for a given input.
- Time per file
- Figures 1, 2, 5 and 6 add measured times to a modelled transfer: compressed bytes ÷ bandwidth plus the median decompression time (download), or the median compression time plus compressed bytes ÷ bandwidth (upload); uncompressed is original bytes ÷ bandwidth. 1 Mbps = 10⁶ bits per second, steady; latency is ignored and nothing overlaps, as streaming would. Server-side compression on the fly is not modelled. A configuration is named fastest over a band only where it beats the next by more than 3% in every capture; stated bandwidths are round values inside that band. Claims are of the levels tested: static files often use higher ones (brotli 11, zstd 19+). Times are the payload's alone, with codecs loaded; Browser gzip is CompressionStream and DecompressionStream. Slower devices move crossovers lower.
- Storage
- Figure 6's storage column is what it costs to keep 1 TB (10¹² bytes) of original data stored compressed that way: one copy, one month, in S3 Standard, us-east-1, first 50 TB at $0.023 per GiB-month (AWS bills GB = 2³⁰ bytes; https://aws.amazon.com/s3/pricing/, checked November 2026); uncompressed is $21.42. "vs previous winner" compares sizes with the winner of the next lower band. Replicas and other storage classes change it; request charges don't depend on compression and are left out. Storage depends on the client's compressed size if the server keeps those bytes; recompression changes the stored size. If stored bytes are served back out, S3 egress, charged on each download, typically costs more per byte than a month of storage. Validating client-compressed uploads: cap the compressed size, the decoder's memory (the zstd window, say) and CPU time; allow listed formats only; hash and scan after decompressing.
- Speed relative to Chrome
- Figure 3 divides each configuration's median speed in Firefox or Safari by its median in Chrome on the same machine, capture 1 with capture 1 and capture 2 with capture 2. Dots are the median of those ratios over every input, level and capture; lines span them all.
- About the same
- Configurations run one after another, round-aligned. The figures compare them by their medians (not paired). Two times are about the same when their medians are less than 3% apart. A configuration is fastest only when the next takes more than 3% longer, with the same winner, in every capture. Ranges are the observed min–max, not confidence intervals.
- Fastest decoders
- Each decoder reads its own library's output (zune-inflate reads miniz_oxide's). In a separate test (six decoders on five encoders' raw-deflate streams, dickens and x-ray, Node 26 on an Apple M4 Pro), the stream alone moved one decoder's speed up to about 1.2× at level 6, and up to 1.7× at level 1, so two decoders can sit up to 1.2² ≈ 1.44× apart from their streams alone. Decoders are compared at level 6: a group is clearly fastest only when even its slowest is more than 1.44× faster than the next decoder, in both captures; its members are within 1.44× of its fastest.
- Reproduction
- A finding becomes a figure title or main point only where every capture makes it; the details show capture 1.
- Build
- rustc 1.99.1 (58cf8f9c3 2026-10-13); wasi-sdk-29; wasm-opt version 133 (version_133); wasm-bindgen 0.2.130. Release profile: opt-level 3, LTO true, codegen-units 1, target features none (no simd128), wasm-opt -Os.
- Libraries
- lz4_flex 0.14.0, zstd 0.14.3 (libzstd 1.5.7), miniz_oxide 0.9.1, zune-inflate 0.2.54, libdeflater 1.26.1 (libdeflate-sys 1.26.1), zlib-rs 0.6.8 (via flate2 1.1.11), brotli 9.1.1 (decoder brotli-decompressor 6.3.0), pako 3.0.3, fflate 0.8.3.
- Commit
ef16b9543512- Artifacts
10 wasm modules, SHA-256
br_read_bg.wasm218 kB600d59953ee72a25a99f88e97028b7b170ba9bc725394d017ad6f084329714ebbr_write_bg.wasm945 kB5ca1c6f2cdfff4f1b0c79bec77176c14545c9dddb17e4ec94d5eaa413522ce2ecorpus_bg.wasm139 kBf35f70fcf55f73022df9ef8a7695abe6be8c2eaccd95237e02a3c1ca5eee7f1blibdeflate_bg.wasm75.6 kB417c7fb708c818cb3fad6d2e381b6fd62195d69e39ea6b8fdb4fd1baafa3be84lz4_bg.wasm59.6 kBebe01820ec888b26467fd0303cfaed85cea6c29f6c7980d4e088525a632646ddminiz_bg.wasm58.3 kBa54a2d4ab22b65d6b80d4f36ba181c2dab7632c26be1a166ab1ca5893acc4395zlib_rs_bg.wasm123 kBa738fdcf343c7046d34c98896be663ac15df54b9a711bd4b20513db39981d69czstd_read_bg.wasm137 kB29f5cc2a376640aec12ba67b5cf7ad25c344f42688f552ac3759975f101eb4cczstd_write_bg.wasm464 kBb9dcb6036757d7b1ac36f4516beabf0cdf25f0c1439e63af7839d3f4b9b337ffzune_bg.wasm29.6 kB4289807d5f9e9991bd3bc220aa8fbe8e8e01a12b5a18925ac93c7ddaee3e8507
- Corpus
- Silesia corpus subset (1 of 12 files), checked against pinned SHA-256 hashes when packed and by length when loaded.
- dickens 10,192,446 bytes
b24c37886142e11d0ee687db6ab06f936207aa7f2ea1fd1d9a36763c7a507e6a
- dickens 10,192,446 bytes
- Raw data
- Raw per-round data (JSON)
Reference edition 2026-11-08· Apple M4 Pro (arm64), macOS 26.5.2 (25F84)· Chrome 156, Firefox 151 and Safari 26· 10 samples per configuration, 2 captures· Method· Raw data (JSON)
4. Measure on this machine
Run the same configurations on a Silesia file or your own. Outputs are checked byte for byte before timing.
25 configurations