@jsquash/avif 2.1.1 ships two copies of the same AVIF encoder (libavif v1.0.1), and on disk they are about 50 KB apart. avif_enc.wasm works on one thread, and avif_enc_mt.wasm spreads the work over several. Some background first. AVIF is the image format built on the AV1 video codec, and most browsers I tried will decode it but will not write it. The writing is done by an encoder, the program that turns raw pixels into the compressed file, shipped to the page as WASM, which is compiled code the browser runs next to JavaScript. With the one-thread file, a 12-megapixel photo took close to four seconds to encode on my test page. I wanted to know how much of that the second file would give back.
All of this was timed on one Apple M4 Mac running macOS, in Chromium 149, Chrome 155 and WebKit 26.5. No phones, no Windows, no release Safari. The number I report is the encode() call alone, inside a Web Worker (a background script, so the page stays responsive), three runs per cell, at the package defaults of quality 50 and speed 6. Decoding and resizing the source are outside the timer. The photo is a lake scene by Jakub Fryš from Wikimedia Commons (CC BY-SA 4.0), scaled on a canvas to each size.
const glueFile = wantThreads ? 'avif_enc_mt.js' : 'avif_enc.js';
const makeCodec = (await import(`./codec/enc/${glueFile}`)).default;
const codec = await makeCodec({ noInitialRun: true });
const startedAt = performance.now();
const avif = codec.encode(rgbaPixels, width, height, { ...avifDefaults, speed });
const encodeMs = performance.now() - startedAt;
At the default speed the threaded build was 2 to 3 times faster
At 12 MP (4256×2820) the single-threaded build took 3802 / 3754 / 3747 ms in Chromium and the threaded one took 1441 / 1410 / 1468 ms, so about 3.8 s became about 1.4 s. WebKit went from 4022 / 3963 / 3930 ms to 1279 / 1260 / 1251 ms, and Chrome sat within a few dozen milliseconds of Chromium. The gap held at 2 MP, where Chromium went from 696 / 696 / 673 ms to 327 / 324 / 323 ms. It also held on the slowest picture I fed it, an 8.3 MP screenshot of an admin-style table I rendered myself: 13662 / 13635 / 13613 ms against 5470 / 5426 / 5374 ms. Taking the middle run at each size of the lake photo, the threaded build came out 1.9 to 2.7 times faster in Chromium and Chrome and 2.0 to 3.2 times faster in WebKit. Picking the threaded file cost almost nothing up front. Gzipped on my machine the two WASM files are 1,115,948 and 1,130,150 bytes, and going from import() to a ready encoder took 14 / 13 / 12 ms and 35 / 33 / 30 ms in Chromium, with no download in that figure.
At the two fastest speeds I tried the threads did not help
speed is libavif's effort setting, and a higher number means a quicker encode. I ran the 2 MP lake photo through five values in Chromium. At speed 4 the threaded build still led clearly, 2746 / 2591 / 2625 ms against 7682 / 7563 / 7510 ms. Then the order flipped. At speed 8 the single-threaded build took 230 / 163 / 161 ms and the threaded one 239 / 174 / 173 ms. At speed 10 it was 156 / 103 / 100 ms against 204 / 149 / 146 ms, so the build with more threads was the slower one in all three runs. I only have the timings and did not investigate why.
The chart shows the single-threaded middle run at each speed. Look at the bottom two bars: 163 ms and 103 ms are the numbers the threaded build failed to beat. Those files were smaller too, but I did not measure their image quality.
The threaded build needs two response headers
The threaded file needs a page that is cross-origin isolated, a state the browser grants when the document is served with these two headers. My test server sent them, and crossOriginIsolated read true in every browser I used.
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
What happens when the threaded build is loaded without them is something I did not test, so I have no failure mode to describe. That matters to me more than the milliseconds, because headers on the document are a change to the whole page and not to my export component.
To see how a shipping product handles the loading, the tool I used was ImgIng (https://imging.ai/), a browser-based image converter, in Chromium 149 with one run per image size. The network observations come from its Chinese-language site and the screenshot from the English one. It fetched avif_enc_mt.wasm, the threaded build, at 1,135,337 bytes over gzip, and the request went out the moment I selected AVIF, before I pressed convert. I did not read its code, and its on-screen wait includes reading the image and updating the page, a different clock from my encode() timer.
In the screenshot the AVIF button is tagged "On-demand WASM" and the main button reads "Downloading module…". For my own page the outcome is narrower than I expected: at speed 6 the threaded build is the one I would load, and if I moved to speed 8 or above I would time both files again before asking for the headers.

Top comments (0)