SIMD and wasm‑opt in WebAssembly builds: November 2026
- Reference machine
- Apple M4 Pro (arm64) · macOS 26.5.2
- Browsers
- Chrome 156, Firefox 151 and Safari 26
- Samples
- 15 rounds per row, two captures
Only the Apple M4 Pro (arm64) has been measured, not x86-64.
Measure on this machine1. Main points
- On arm64, simd128 cut zlib-rs deflate L1's time by up to 18.4%, but slowed zstd decode by up to 7.6% in Firefox (Figure 1).
- Skipping wasm-opt cut brotli encode's time by up to 18.5% in Chrome and Safari, but slowed 3 workloads by up to 9.6% (Figure 2).
- wasm-opt -Os shrank all 5 modules by up to 33.1 kB after compression (Figure 3).
2. SIMD and speed
Figure 1. simd128 cut zlib‑rs deflate L1's time by 6.3%–18.4% on 2 of 3 inputs, in all 3 browsers
simd128 build's time ÷ the scalar build's in the same round, both at wasm-opt -Os; 15 rounds.
- Claimed in both captures
- Not claimed
- 95% CI
- Min–max
- ±3% threshold
Arrow keys move between points; Enter pins one so its values stay shown; Escape clears.
Notes: 10 of 22 rows, with no claim and browsers agreeing, are in Table 1 only. zstd decode in Safari: A/A control failed, so grey (Figure 4). 3 claims held in one capture only, so not counted.
Why the processor matters
zlib-rs's compare256, deflate's match-length loop, ends each 16-byte step in i8x16.bitmask, one instruction on x86-64 but a sequence on arm64.
This may give SIMD a larger gain in zlib-rs deflate on x86-64, which has not been measured here.
;; zlib-rs compare256, wasm32 + simd128, per 16 bytes
(i8x16.eq a b) ;; 0xff where bytes match
(i8x16.bitmask ...) ;; one bit per byte
;; x86-64 (V8): one instruction each
pcmpeqb xmm0, xmm1
pmovmskb eax, xmm0
;; arm64 (V8): no single equivalent for i8x16.bitmask;
;; V8 emits a multi-instruction sequence (which one
;; depends on the CPU's features)3. wasm-opt and speed
Figure 2. Skipping wasm‑opt cut brotli encode's time by 4.3%–18.5% on all 3 inputs, in Chrome and Safari
Scalar build without wasm-opt: its time ÷ the shipped build's (wasm-opt -Os) in the same round; 15 rounds.
- Claimed in both captures
- Not claimed
- 95% CI
- Min–max
- ±3% threshold
Arrow keys move between points; Enter pins one so its values stay shown; Escape clears.
Notes: 11 of 22 rows, with no claim and browsers agreeing, are in Table 1 only. zstd decode in Safari: A/A control failed, so grey (Figure 4). 1 claim held in one capture only, so not counted.
Without SIMD, wasm-opt -O3 instead of -Os cut lz4_flex decode's time up to 5.5% on samba in Chrome and on x-ray and samba in Safari; no other workload changed in both captures (Table 1).
4. wasm-opt, SIMD and download size
Figure 3. wasm‑opt ‑Os shrank all 5 modules; simd128 grew all 5
Change in brotli-compressed bytes per module; exact and the same in every browser.
Arrow keys move between points; Enter pins one so its values stay shown; Escape clears.
Source: Build manifests of the modules this page runs (scripts/build-variants.ts). rustc 1.99.1 (58cf8f9c3 2026-10-13); wasm-opt version 133 (version_133)
Notes: Raw and gzip bytes, and the -O3 builds: Table 2.
5. The A/A control
Figure 4. A/A control: within ±2.2% in Chrome and Firefox; failed for the zstd decoder in Safari
Time of a second, byte-identical instance of the shipped build ÷ the first's, pooling each workload's inputs and both captures. Where it fails, nothing is claimed for that workload in that browser.
- Control failed
- Passed
- 95% CI
- Min–max
- ±3% threshold
Arrow keys move between points; Enter pins one so its values stay shown; Escape clears.
6. All configurations
| Workload · input | Shipped, per run | scalar no wasm-opt | scalar -O3 | simd128 no wasm-opt | simd128 -Os | simd128 -O3 | A/A |
|---|---|---|---|---|---|---|---|
| zstd decodedickens | 10.1 ms | 1.0371.02–1.04 | 1.0611.05–1.07 | 1.0381.02–1.05 | 1.0091.00–1.03 | 1.0111.01–1.02 | 0.9990.99–1.01 |
| x-ray | 11.0 ms | 1.0241.01–1.03 | 1.0040.99–1.01 | 1.0040.99–1.01 | 0.9960.99–1.01 | 1.0030.99–1.01 | 0.9940.99–1.00 |
| samba | 13.6 ms | 1.0221.00–1.03 | 0.9880.98–1.01 | 0.9960.98–1.01 | 0.9620.95–0.97 | 0.9860.97–0.99 | 0.9940.99–1.01 |
| zlib-rs inflatedickens | 15.9 ms | 1.0141.00–1.03 | 1.0030.99–1.02 | 0.9590.95–0.97 | 0.9550.95–0.96 | 0.9500.95–0.97 | 1.0010.99–1.01 |
| x-ray | 24.1 ms | 1.0171.01–1.02 | 0.9991.00–1.00 | 1.0201.02–1.03 | 1.0121.00–1.02 | 1.0121.00–1.02 | 1.0051.00–1.01 |
| samba | 24.7 ms | 1.0171.00–1.02 | 1.0081.00–1.02 | 0.9240.92–0.93 | 0.9130.91–0.92 | 0.9160.91–0.93 | 1.0031.00–1.01 |
| zlib-rs deflate L1dickens | 50.6 ms | 1.0091.00–1.02 | 1.0070.99–1.01 | 0.8630.85–0.87 | 0.8590.85–0.87 | 0.8620.85–0.87 | 1.0060.99–1.02 |
| x-ray | 57.6 ms | 0.9740.94–0.99 | 0.9740.95–1.01 | 0.9730.95–0.99 | 0.9600.93–0.98 | 0.9670.94–0.98 | 1.0000.99–1.01 |
| samba | 68.3 ms | 0.9930.99–1.00 | 1.0040.99–1.01 | 0.8210.81–0.83 | 0.8160.81–0.84 | 0.8250.81–0.83 | 0.9970.99–1.01 |
| zlib-rs deflate L6dickens | 151 ms | 1.0131.01–1.02 | 0.9950.99–1.00 | 0.9940.99–1.00 | 0.9790.98–0.99 | 0.9820.98–0.98 | 0.9991.00–1.00 |
| x-ray | 126 ms | 1.0961.09–1.10 | 0.9990.99–1.00 | 1.0611.06–1.07 | 0.9720.97–0.98 | 0.9730.96–0.98 | 0.9980.99–1.00 |
| samba | 194 ms | 1.0251.02–1.03 | 1.0011.00–1.00 | 0.9740.97–0.98 | 0.9480.95–0.95 | 0.9530.95–0.96 | 1.0031.00–1.01 |
| zlib-rs deflate L9dickens | 294 ms | 0.9950.99–1.00 | 0.9930.99–1.00 | 0.9760.97–0.98 | 0.9820.98–0.98 | 0.9780.98–0.98 | 1.0021.00–1.01 |
| x-ray | 194 ms | 0.9790.98–0.98 | 0.9790.98–0.98 | 0.9580.95–0.96 | 0.9900.99–0.99 | 0.9720.97–0.98 | 1.0001.00–1.00 |
| samba | 392 ms | 0.9890.99–0.99 | 0.9880.99–0.99 | 0.9530.95–0.96 | 0.9630.96–0.97 | 0.9600.96–0.96 | 1.0041.00–1.01 |
| lz4_flex decodedickens | 4.65 ms | 0.9860.98–0.99 | 0.9770.97–0.98 | 0.9870.98–0.99 | 0.9810.98–0.99 | 0.9830.97–0.98 | 1.0030.99–1.01 |
| x-ray | 2.11 ms | 0.9850.97–1.00 | 0.9760.96–0.98 | 0.9840.97–1.00 | 0.9870.98–1.00 | 0.9660.95–0.98 | 1.0041.00–1.01 |
| samba | 7.57 ms | 0.9870.98–0.99 | 0.9650.96–0.97 | 0.9770.97–0.98 | 0.9810.97–0.98 | 0.9520.95–0.97 | 1.0010.99–1.01 |
| brotli encodedickens | 149 ms | 0.9570.95–0.96 | 0.9900.99–1.00 | 0.9550.95–0.96 | 0.9860.98–0.99 | 0.9870.98–0.99 | 1.0000.99–1.00 |
| x-ray | 149 ms | 0.9300.93–0.93 | 0.9880.99–0.99 | 0.9210.92–0.92 | 0.9700.97–0.97 | 0.9680.96–0.98 | 0.9991.00–1.00 |
| samba | 172 ms | 0.9490.94–0.95 | 0.9860.98–0.99 | 0.9410.94–0.94 | 0.9860.98–0.99 | 0.9840.98–0.99 | 0.9990.99–1.00 |
| XML (dlmalloc)xml | 46.8 ms | 0.9970.99–1.00 | 1.0011.00–1.01 | 0.9970.99–1.00 | 0.9980.99–1.00 | 0.9970.99–1.00 | 1.0071.00–1.01 |
| Module · SIMD | No wasm-opt | wasm-opt -Os | wasm-opt -O3 | SIMD instructions none · -Os · -O3 |
|---|---|---|---|---|
| zstd decoderscalar | 43,227145,543 · 53,932 | 41,465137,334 · 51,179 | 41,265137,379 · 51,131 | 0 · 0 · 0 |
| simd128 | 44,653148,212 · 55,859 | 42,612140,050 · 52,524 | 42,496140,173 · 52,634 | 658 · 664 · 666 |
| zlib-rsscalar | 47,980130,845 · 57,481 | 47,217123,119 · 56,415 | 47,124123,255 · 56,382 | 0 · 0 · 0 |
| simd128 | 48,466130,831 · 57,655 | 47,670123,194 · 56,427 | 47,474123,537 · 56,376 | 523 · 533 · 547 |
| lz4_flexscalar | 23,31066,313 · 27,512 | 22,40459,668 · 26,487 | 22,42360,129 · 26,496 | 0 · 0 · 0 |
| simd128 | 23,62766,978 · 27,960 | 22,56260,601 · 26,624 | 22,54061,056 · 26,716 | 184 · 183 · 183 |
| brotli encoderscalar | 378,6541,007,336 · 518,206 | 345,521945,132 · 527,136 | 345,923945,156 · 527,080 | 0 · 0 · 0 |
| simd128 | 379,7461,008,484 · 518,301 | 347,733949,621 · 526,455 | 394,572950,835 · 527,290 | 2,180 · 2,377 · 2,387 |
| XML processingscalar | 16,95848,763 · 20,560 | 16,04445,905 · 19,315 | 15,95645,984 · 19,282 | 0 · 0 · 0 |
| simd128 | 17,03048,614 · 20,568 | 16,05445,923 · 19,272 | 16,02246,000 · 19,259 | 104 · 115 · 115 |
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:47–21:53 UTC. Capture 1 of 2.
- Clock
- Cross-origin isolated. Timer resolution in a worker 5 µs (median step; finest 5 µs).
- Method
- 15 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; ratios are paired by round.
- Workloads
- zstd decode: libzstd 1.5.7 (zstd-sys) decompressing a level-3 frame.
- zlib-rs inflate: zlib-rs (via flate2) inflating a level-6 raw deflate stream; explicit simd128 chunk copies.
- zlib-rs deflate L1: zlib-rs (via flate2) deflating at level 1 (deflate_quick); explicit simd128 compare256, slide_hash.
- zlib-rs deflate L6: zlib-rs (via flate2) deflating at level 6 (deflate_medium); explicit simd128 compare256, slide_hash.
- zlib-rs deflate L9: zlib-rs (via flate2) deflating at level 9 (deflate_slow); explicit simd128 compare256, slide_hash.
- lz4_flex decode: lz4_flex frame decoder; no explicit SIMD, autovectorization only.
- brotli encode: the brotli crate compressing at quality 4 (22-bit window); autovectorization only.
- XML document processing (dlmalloc): Application code: the allocation page's workload on Rust's default allocator, parsing, walking and dropping each of the Silesia xml file's 21 XML documents. No explicit SIMD; the simd128 build autovectorizes some loops.
- Builds
- Six builds of each module: simd128 off or on, then no wasm-opt,
-Osor-O3. Every one is Rust's release profile (opt-level 3 (default), lto = true, codegen-units = 1). SIMD on:-C target-feature=+simd128and, for the C code,-msimd128. Thenwasm-bindgen --target web --remove-name-sectionandwasm-opt -Osor-O3. The shipped build, which every other page runs, is scalar withwasm-opt -Os. - Pairing
- Each round runs all seven variants (the six builds and the A/A instance) in a rotating order, so every comparison is a ratio from the same round. Figures and main points count a claim only where both captures make it.
- Sizes
- Raw bytes of each .wasm after wasm-bindgen and wasm-opt, then gzip level 9 and brotli quality 11 of the same file. Exact, so they need no interval, and the same in every browser. simd128 made every module 0.1%–2.8% larger at wasm-opt -Os (brotli bytes). SIMD instructions are a static count.
- Mechanism
- zlib-rs 0.6.8,
src/deflate/compare256.rs(the wasm32 and NEON paths); V8 at commit 63481c19 (7 November 2026),kX64IBitMaskinsrc/compiler/backend/x64/code-generator-x64.ccandMacroAssembler::I8x16BitMaskinsrc/codegen/arm64/macro-assembler-arm64.cc. The lowering is V8's (Chrome); Firefox and Safari compile wasm with their own compilers, not covered here. - Recaptured
- XML document processing (dlmalloc) was recaptured on 9 November 2026, 01:52–01:53 UTC, in Chrome, Firefox and Safari, and replaced the original rows. Its two captures per browser ran back to back in that one session, about 40 seconds apart, so their agreement shows back-to-back reproduction, not independent sessions.
- Raw data
- Chrome 156, Apple M4 Pro (arm64), capture 1 of 2 (JSON)
- Chrome 156, Apple M4 Pro (arm64), capture 2 of 2 (JSON)
- Firefox 151, Apple M4 Pro (arm64), capture 1 of 2 (JSON)
- Firefox 151, Apple M4 Pro (arm64), capture 2 of 2 (JSON)
- Safari 26, Apple M4 Pro (arm64), capture 1 of 2 (JSON)
- Safari 26, Apple M4 Pro (arm64), capture 2 of 2 (JSON)
- 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
- zstd 0.14.3 (libzstd 1.5.7), zlib-rs 0.6.8 (via flate2 1.1.11), lz4_flex 0.14.0, brotli 9.1.1 (decoder brotli-decompressor 6.3.0).
- Commit
ef16b9543512- Artifacts
32 wasm modules, SHA-256
corpus_bg.wasm139 kBf35f70fcf55f73022df9ef8a7695abe6be8c2eaccd95237e02a3c1ca5eee7f1bvariants/alloc/off-none/alloc_bg.wasm48.8 kBab2887573b311b69ff8536abd581ec3a560815a4c547ae6d2060588ccaa262b8variants/alloc/off-O3/alloc_bg.wasm46.0 kBa04a127b213d470ce9573c05606d9af933a25a2102a60dc1144d330e541cd3b1variants/alloc/off-Os/alloc_bg.wasm45.9 kB507c0dd00c71371c4fbcd5a45aa96fca5728eae2ae20d598dfa3475a7148c6c3variants/alloc/on-none/alloc_bg.wasm48.6 kBa76f3b94028727f68808b0f468261ecc7ba56833bd1619600867042d7331eb60variants/alloc/on-O3/alloc_bg.wasm46.0 kB64db27d174fcbb18f60734f3dde24ce8af7fa9ffa83c4a6943e2769fad2d80d7variants/alloc/on-Os/alloc_bg.wasm45.9 kB19f91020ca38978c64dddb84e420771a8277123da9bb5996d4f22689f68f3be2variants/br_write/off-none/br_write_bg.wasm1.01 MBb67075e0d5b787b9d7298bc0cf8656981a771c3507975b450fb0b9f3658ca803variants/br_write/off-O3/br_write_bg.wasm945 kB5e7d92a860070f4ce7cb054db55bc0d96e1847b567baf4a02d2cab26982bac14variants/br_write/off-Os/br_write_bg.wasm945 kB5ca1c6f2cdfff4f1b0c79bec77176c14545c9dddb17e4ec94d5eaa413522ce2evariants/br_write/on-none/br_write_bg.wasm1.01 MB334e8d106fefeda519ec915265b9d2e52dac0391b97152d204f5d8997b796af9variants/br_write/on-O3/br_write_bg.wasm951 kB2d041090b38371dee7097b29182387dd438544235c2c16a3abddc0332ef21e76variants/br_write/on-Os/br_write_bg.wasm949 kBd0852a8ec562a1adc8faa16247787dc5d2a3a1ef516b484d745c8dad6f1cea81variants/lz4/off-none/lz4_bg.wasm66.2 kB6217191dda9baeb80f2714bc330c7fa63b541464ceb9fb14d01cf50be41e2244variants/lz4/off-O3/lz4_bg.wasm60.1 kB470263fb17ff3b89fef5fcf7f5ff7d1a7763b9109a967c1982b4e0f1c0618a83variants/lz4/off-Os/lz4_bg.wasm59.6 kBebe01820ec888b26467fd0303cfaed85cea6c29f6c7980d4e088525a632646ddvariants/lz4/on-none/lz4_bg.wasm66.9 kB6751223b53f1c4b501418d71d5cf765315c37cf256d7d2e849339bce8c64ba16variants/lz4/on-O3/lz4_bg.wasm61.0 kB9bc4dfd68a3de30d0d29abb2e3df0a79430b5a1e12532bf33721bfc3cae89450variants/lz4/on-Os/lz4_bg.wasm60.5 kBf2a2b9037cf9fb8e3c52b6bd32cd23de17929572dd8fc99096557a082b8ac0a5variants/zlib_rs/off-none/zlib_rs_bg.wasm131 kBe1c5d4788c99c4dcbb1b6467b7853f6598564c2a420d8cd509c816f27d8d3b51variants/zlib_rs/off-O3/zlib_rs_bg.wasm123 kBb4a9b727c2bd23ddb3a062b4ce4fd1b26c1e7b1c87bb98a78acb1991aedbc7b1variants/zlib_rs/off-Os/zlib_rs_bg.wasm123 kBa738fdcf343c7046d34c98896be663ac15df54b9a711bd4b20513db39981d69cvariants/zlib_rs/on-none/zlib_rs_bg.wasm131 kB812730d272912bdd676845b07ca51b978a40ef2c2910887ce5cf36d0804b38d9variants/zlib_rs/on-O3/zlib_rs_bg.wasm123 kB1b2964f7dffe1502aa0f419534ef990b97c9baf3304e7f5093f2b9a9fd02cce9variants/zlib_rs/on-Os/zlib_rs_bg.wasm123 kBca511fcb65faa6cbc87fcb35cbeb2bd7f4939f84371bf7c9963570821e6e9da1variants/zstd_read/off-none/zstd_read_bg.wasm146 kB4157ae9ede9414c99a8ecc7470719a9003624917575aee86e5cd3dc3ceac828bvariants/zstd_read/off-O3/zstd_read_bg.wasm137 kB612669c809a6da58f075454a3fe4f8162f75fc34c8e55c21a51062d690f2500fvariants/zstd_read/off-Os/zstd_read_bg.wasm137 kB29f5cc2a376640aec12ba67b5cf7ad25c344f42688f552ac3759975f101eb4ccvariants/zstd_read/on-none/zstd_read_bg.wasm148 kBdfd55dc429050955e70286a86b5dfd372acd702f110b6a5cc8b2c2b5bbeab17bvariants/zstd_read/on-O3/zstd_read_bg.wasm140 kBf2784feb7b2b1b3e1ae89033bd393658eceb355c38e2aa70f6bb9b80df58a538variants/zstd_read/on-Os/zstd_read_bg.wasm140 kB73c1ae63834110234945de0a4f5462fc46bea1a2052f3b4ed34a4cf2a5e85328zstd_write_bg.wasm464 kBb9dcb6036757d7b1ac36f4516beabf0cdf25f0c1439e63af7839d3f4b9b337ff
- Corpus
- Silesia corpus subset (3 of 12 files), checked against pinned SHA-256 hashes when packed and by length when loaded.
- dickens 10,192,446 bytes
b24c37886142e11d0ee687db6ab06f936207aa7f2ea1fd1d9a36763c7a507e6a - x-ray 8,474,240 bytes
7de9fce1405dc44ae5e6813ed21cd5751e761bd4265655a005d39b9685d1c9ad - samba 21,606,400 bytes
93ba07bc44d8267789c1d911992f40b089ffa2140b4a160fac11ccae9a40e7b2
- 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· 15 rounds per row, two independent captures each· XML (dlmalloc) captured back to back· Method· Raw data (JSON)
7. Measure on this machine
Each row times all six builds and the A/A instance.
This choice sampled for about 73 seconds on the reference machine in Chrome 156, plus a few seconds per row to load, calibrate and warm up.