Skip to content
scroll to zoom · drag to pan

WASM WebP for batches

WebKit cannot encode WebP from a canvas, so on Safari and every iOS browser the compressor falls back to JPEG. Online converters produce WebP in those same browsers anyway. They ship their own encoder, compiled to WebAssembly, and never touch canvas.toBlob.

We could too. Here is the measurement, because the answer changes depending on how many files are coming.

Same 4.9 MP source, same target dimensions (2560 px), same quality (82). @jsquash/webp — the same libwebp build that would run in the browser:

size time
JPEG, native encoder — what WebKit gives you today 621 kB 109 ms
WebP, WASM encoder 364 kB 489 ms
−41.4 % 4.5× slower

And the encoder itself, fetched once and then cached:

file size
webp_enc_simd.wasm 337 kB
webp_enc.wasm (no SIMD) 275 kB

You save 257 kB and download a 337 kB encoder. Net loss on bytes, plus 380 extra milliseconds. For a single upload the JPEG fallback is simply the right answer.

This is the case that matters, and it is the common one — someone picking a camera roll selection.

saved 30 × 257 kB = 7.5 MB
encoder cost 337 kB, once
net data saved ≈ 7.2 MB
extra encode time 30 × 380 ms ≈ 11 s on a laptop

Break-even is the second photo. After two images the encoder has paid for itself in bytes, and everything after that is pure saving.

The time cost mostly disappears, and exactly when it matters

Section titled “The time cost mostly disappears, and exactly when it matters”

11 seconds of extra encoding sounds bad until you notice that encoding and uploading can overlap: photo N+1 encodes while photo N is still uploading.

Do the arithmetic on a realistic uplink. A 364 kB WebP on 4G at ~250 kB/s takes 1.45 s to upload, against 489 ms to encode. The encode finishes long before the upload does, so in a pipelined batch the WASM encoder is free — it runs in dead time the network was going to waste anyway.

And the slower the connection, the more true that gets:

uplink 7.2 MB saved ≈ encode overhead verdict
3G / congested cellular (~60 kB/s) 2 min hidden behind uploads big win
4G (~250 kB/s) 29 s hidden behind uploads clear win
fast wifi (~10 MB/s) 0.7 s ~11 s, partly exposed not worth it

So the rule is not really “how many files” — it is “is the network slower than the encoder?” For a batch on cellular, always yes. On fast wifi, no, and there the bytes did not matter anyway.

Why the SDK is the right place to decide this

Section titled “Why the SDK is the right place to decide this”

The SDK already knows everything the decision needs, before a single byte moves:

  • how many files — compressImages() takes the array;
  • the total size — it has hashed them;
  • whether the engine can encode WebP natively — it already probes this to choose the fallback;
  • the connection, via navigator.connection where available.

That is the whole argument for putting this in an SDK rather than leaving it to each app: the caller says “here are 30 photos”, and the library is the only layer that can see the batch, the engine and the link at the same time.

Delivery is WebP either way. The server re-encodes every variant, so this only ever affects upload bytes and the format of the archived original. Nobody’s page gets faster — the user’s upload gets cheaper, which on a metered phone plan is the thing they actually feel.