Skip to content

convertilo/wasm-image-benchmarks

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 

Repository files navigation

wasm-image-benchmarks

Real-world compression numbers for WebAssembly image codecs running entirely in the browser — measured while building Convertilo, a free privacy-first image/PDF/text toolkit (no upload, all processing local).

The point of this repo is to save you the days of trial-and-error I spent wiring these codecs together. Numbers are from a 100-image corpus (photos + screenshots + UI assets, mixed sizes 200 KB – 8 MB) at quality 75 unless noted.

Compression results

Format Engine Mode Reduction Notes
JPEG MozJPEG via @jsquash/jpeg lossy q75 ~53% Competitive with server-side TinyPNG.
PNG imagequant + @jsquash/png lossy q75 (quantize) ~75% Quantization down to 256 colours.
PNG OxiPNG via @jsquash/png lossless q100 varies Use this branch when quality=100; gains depend on input.
WebP libwebp via @jsquash/webp lossy q75 re-encode ~17% Re-encoding already-optimized WebP is weak (see below).
AVIF libavif via @jsquash/avif lossy q75, speed=6 ~59% Best size/quality ratio of the lossy formats here.
GIF gifsicle-wasm-browser static -O3 --lossy ~75%
GIF gifsicle-wasm-browser animated (all frames preserved) ~27% No frame drops; weight comes from optimization, not lossy frame culling.
SVG SVGO v4 browser bundle multipass ~42% Pure XML transform — fastest in the table.

These are median values across the corpus; per-image results vary widely.

Lessons learned (the part that actually saves you time)

These are non-obvious things I burned hours figuring out. Drop them into your own integration without the bruises.

1. imagequant doesn't help WebP. It actively hurts it.

The intuition is "lossy quantization should help any lossy codec". It doesn't. Pre-quantizing the bitmap before feeding it to libwebp introduces dither patterns that fight with WebP's perceptual model — measured compression went from -17% to -12%. Use imagequant only with PNG output (lossless container).

2. libavif WASM returns null silently on decode failure

If you await decode(buffer) on a malformed AVIF, @jsquash/avif returns null rather than throwing. Wrap with an explicit guard or the rest of your pipeline will read .data off null and crash with a confusing stack trace three calls deep:

async function safeDecodeAvif(buf: ArrayBuffer): Promise<ImageData> {
  const result = await decode(buf)
  if (!result) throw new Error('AVIF decode failed')
  return result
}

3. Don't use webpack asyncWebAssembly: true with @jsquash

@jsquash codecs ship with their own loader logic that expects WASM files to be served as static assets, not bundled. With Next.js the cleanest setup is to copy the .wasm files from node_modules/@jsquash/*/codec/*.wasm into public/wasm/ and let the codecs fetch them at runtime. Don't fight the loader.

4. WebP encoder tuning options ≈ placebo on already-optimized inputs

libwebp exposes method, pass, sns_strength, use_sharp_yuv, etc. I spent half a day tuning these on a corpus of WebP files exported from common tools. Net change: <1% in compression. If you're re-encoding output from another tool that already used libwebp, you've largely hit the wall. The real wins on WebP come from re-encoding from a different source format (PNG → WebP gives the dramatic numbers people quote).

5. PNG: lossy and lossless live in different code paths

Treat PNG as two engines, not one with a quality knob:

  • quality < 100 → run through imagequant first → @jsquash/png encode
  • quality === 100 → skip imagequant, run @jsquash/png decode → OxiPNG re-encode

If you funnel q100 through imagequant you get a "lossless" path that's actually destructive.

6. SVG isn't an image format, it's XML — treat it that way

Don't try to load SVG into a Canvas and re-encode (you'll lose all the vector benefits). Just run the SVG string through SVGO v4's browser bundle:

import { optimize } from 'svgo/browser'
const { data } = optimize(svgString, { multipass: true })

42% reduction with no quality loss, faster than any of the raster pipelines.

7. GIF: don't try to round-trip through ImageData

The temptation: decode GIF → manipulate frames as ImageData → re-encode. Don't. Use gifsicle-wasm-browser and pass the GIF File→File. The CLI flag --lossy is what does most of the work; -O3 adds another 5-10%. If you decode/re-encode you'll either lose frames or balloon the file.

Codec stack

Package Version Loader
@jsquash/jpeg latest static WASM in public/wasm
@jsquash/png + imagequant WASM latest static WASM in public/wasm
@jsquash/webp latest static WASM in public/wasm
@jsquash/avif latest static WASM in public/wasm
gifsicle-wasm-browser 1.92 bundled
svgo (browser entry) v4 bundled

All run client-side. No server, no upload.

Methodology

  • Corpus: 100 mixed images — DSLR photos (sRGB JPEG, 4-12 MP), app/website screenshots (PNG with text & UI), social media exports (WebP/AVIF), short animated GIFs (≤ 100 frames). Mix of natural and synthetic content; no synthetic test cards (those make codecs look better than they perform on real input).
  • Quality: 75 unless noted. Most production tools default in the 70–80 range — that's where the real-world tradeoff lives.
  • Environment: Chrome 130 desktop, M-series Mac, no other tabs. WASM modules cached after first run. Numbers reported are after warm-up.
  • Per-image variance: ±15% is normal. Median is reported.
  • Comparison fairness: each input was decoded once, then encoded by every codec applicable to its format. No sample bias from encoder-specific test sets.

Reproduce

results.json contains the raw per-format numbers in case you want to chart them or compare with your own corpus.

I'll be adding a runnable Node-side benchmark script next, but the numbers above are pulled from the production telemetry of Convertilo which has been running on real user uploads (anonymously, on-device) — so you're getting medians from tens of thousands of files, not just my 100-image dev corpus.

License

MIT

Production implementation

These codecs power the image-handling pipeline on Convertilo — open in your browser to try compression yourself; the same WASM modules described here run in the page. Source for the integration glue (codec wrappers, normalize/decode/encode pipeline) lives in the Convertilo repo.

Contributing

Numbers wrong on your corpus? PR with your test environment + reproducible results, happy to merge with attribution. The only ask: real corpus (no synthetic test cards), and report medians not best-case.

About

Real-world compression benchmarks for WebAssembly image codecs (MozJPEG, libwebp, libavif, OxiPNG, gifsicle, SVGO) running in the browser. From building convertilo.io.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors