TL;DR
Modern WebAssembly codecs (MozJPEG, libavif, OxiPNG/imagequant, libwebp, gifsicle, SVGO) now run fast enough in the browser that you can ship a real image-compression tool without uploading anything to a server. I built one (convertilo.io) and benchmarked the codec mix on 100 real-world images. Results below — and a couple of surprises about what doesn't work.
Raw data + repo: github.com/convertilo/wasm-image-benchmarks
The setup
- Corpus: 100 mixed real-world images (photos, screenshots, transparent PNGs, animated GIFs, vector icons)
- Quality: 75 (sane default for lossy)
- Environment: Chrome 130, M-series Mac, warm WASM cache
- Median reduction (per-image variance is ~±15%)
Results
| Format | Engine | Mode | Median size reduction |
|---|---|---|---|
| JPEG | MozJPEG (@jsquash/jpeg) | lossy q=75 | −53% |
| PNG | imagequant + @jsquash/png | lossy palette | −75% |
| PNG | OxiPNG (@jsquash/png) | lossless | varies* |
| WebP | libwebp (@jsquash/webp) | lossy re-encode | −17% |
| AVIF | libavif (@jsquash/avif) | lossy q=75 | −59% |
| GIF (static) | gifsicle-wasm-browser | lossy −O3 | −75% |
| GIF (animated) | gifsicle-wasm-browser | lossy −O3 | −27% |
| SVG | SVGO v4 browser bundle | lossless multipass | −42% |
*OxiPNG depends on whether the input was already optimized — anything from 0% to 60%.
PNG via imagequant and JPEG via MozJPEG hit numbers competitive with server-side TinyPNG. That was the moment I stopped looking for a server.
What actually shipped this
- @jsquash — Sam Sneddon's WASM bindings around mozjpeg, libwebp, libavif, OxiPNG, imagequant. Lazy-loaded per format so the bundle stays tiny: ~250 KB only when the user picks a real file.
- gifsicle-wasm-browser — full gifsicle 1.92 CLI compiled to WASM. Preserves animation frames properly (more on this below).
-
SVGO v4 — works fine in the browser via the bundled
svgo/browserimport. No need for a server.
Three things that didn't work (and saved you the time)
imagequant for WebP/JPEG
imagequant gives PNG the −75% number, so my first instinct was "apply the same quantization to WebP/JPEG before encoding." It actively hurts there: the palette quantization introduces dithering noise that wrecks lossy entropy coding. WebP went from −17% to −12%.
Rule: imagequant for lossless palette PNG only. Don't cross-apply.
WebP knob-twisting
I sunk a few hours into method: 4, pass: 3, sns_strength: 75, use_sharp_yuv: 1 for libwebp. Total impact: <1% extra reduction on already-optimized WebPs. If you're re-encoding WebP that's been touched by another tool, you're squeezing a stone.
GIF via canvas/ImageData
I tried decoding GIF, processing as ImageData, re-encoding. Animation frame timings die in transit. Just use gifsicle as a File → File pipeline and pass -O3 --lossy=80 directly. Don't reimplement what's already a 30-year-old C tool.
The privacy-first angle
The reason I cared at all: image compressors historically upload your files. Receipts, IDs, family photos, contracts — your stuff sits on someone's server. With WASM you can run the same codecs (MozJPEG, libavif, OxiPNG) entirely in the user's tab. Nothing leaves the device.
I learned the hard way that this also requires not loading Yandex.Metrika or Google Analytics before consent, which is a whole other post. The codec part is the easy half.
Reproduce
git clone https://github.com/convertilo/wasm-image-benchmarks
cd wasm-image-benchmarks
# results.json has the medians I cite above
cat results.json
If you want a live implementation rather than a benchmark, all of these codecs are wired up at convertilo.io — drop a file in and watch network: no upload happens.
If you've squeezed more out of any of these (especially WebP — I'm convinced there's more there), I'd love the numbers. Drop a comment.
Top comments (0)