Skip to content
Work / SnapWap

Image compression thatnever leaves your device.

A browser-based image optimizer: six codecs, side-by-side comparison with a diff view and histograms, a target-size optimizer, batch export, and zero uploads. Live at snapwap.vercel.app.

-69%on the poster in the screenshot, WebP at quality 80, no visible loss

What it isOwn product, open source
My roleEverything, design to codecs
When2026
Built withNext.js, WebAssembly, Web Workers
LicenceGPL-3.0, deliberately
StatusLive
SnapWap home page: drop images here or browse, runs in your browser

The problem

Every image compressor on the web asks you to upload the photo first. For a product screenshot, a contract scan, or a family picture, that is a trust problem before it is a technical one. SnapWap does the whole job on your own device, and shows you the original and the result side by side so you can judge the trade-off yourself.

The engineering version
Six codecs (MozJPEG, libwebp, libaom AVIF, libjxl, OxiPNG, libimagequant) compiled to WebAssembly and run inside a dedicated encoder worker. Pixel buffers cross the thread boundary as transferables, so a 29-megapixel image is not cloned. Two debounced lanes (a 40ms preview pass and a 300ms full-effort pass) with sequence IDs, so a slider drag supersedes in-flight encodes instead of queueing sixty of them. A two-phase binary search hits a target file size: quality first, then downscaling if quality 1 still overshoots.
SnapWap from a dropped photo to a smaller file
Drop a photoRead the imagecreateImageBitmap, main threadPick a format and sizeResize and reduceLanczos3, libimagequant (WASM)Compare side by sideMake two versionsMozJPEG / WebP / AVIF / JXL / OxiPNGAll of this happens on your computer.Nothing is uploaded.Web Workers; sequence IDs + AbortController cancelstale jobs when a slider movesDownloadFile on your deviceBlob URL, batch ZIP
The band marks the part that runs off the main thread. Nothing in this diagram touches a server.

What was hard, and what I did about it

Encoding without freezing the page

An AVIF encode can take seconds. Run it on the main thread and every slider feels broken, because the page cannot paint until the encoder returns.

What I did. Moved all encoding into a background worker and made every new change cancel the one before it, so the interface always answers the latest input, not the oldest.

Implementation notes
encoder.worker.ts owns the WASM modules. useEncodePipeline tags each request with an incrementing sequence ID; the worker throws SupersededError for anything older than the newest. A fast lane at 40ms gives a low-effort preview, a final lane at 300ms does the real encode. Buffers are transferred, not structured-cloned.

Hitting an exact file size

People have hard limits: under 100 KB for a marketplace upload, under 2 MB for a form. Dragging a slider and re-checking the size is guesswork.

What I did. Built a target-size optimizer: type the number of kilobytes and it finds the highest quality that fits, and if no quality fits it scales the image down step by step until one does.

Implementation notes
Phase one is a binary search over quality 1 to 100, at most seven encodes. Phase two, if quality 1 still overshoots, walks through downscale factors (90, 75, 60 percent) and repeats phase one at each size.

Getting the licence right

The best palette quantizer available is GPL. Serving that WebAssembly to a browser is conveying it, which has consequences for the whole product.

What I did. Chose GPL-3.0 for the product on purpose, wrote down why, and built the quantizer behind an interface so it can be swapped for a permissive one later without a rewrite.

Implementation notes
Quantizer engines live under src/lib/quantizers. A prebuild script fails the build if a copyleft binary is staged in a build that is meant to exclude it. THIRD_PARTY_NOTICES.md carries verified copyright lines for every codec.

Measuring before claiming

The first release felt slow. The tempting answer was to say GPU, or to quote frame rates. Neither would have been true.

What I did. Checked the shipped codec binaries against the upstream Squoosh builds and found they were the baseline versions, without SIMD or threads. Swapped in the SIMD WebP build, staged the threaded AVIF and JPEG XL builds, and wrote down what was measured and what was not.

Implementation notes
MD5 comparison against the Squoosh codec checkout. GPU is not applicable: MozJPEG, libwebp, libaom and libjxl are serial entropy coders. Threaded builds are held behind measurement so no number ships without a benchmark behind it.
Original and compressed side by side: a 49.87 KB poster at 15.27 KB as WebP, 69% smaller, with both histograms
Original and compressed side by side: a 49.87 KB poster at 15.27 KB as WebP, 69% smaller, with both histograms
SnapWap landing page with the drop zone and supported formats
SnapWap landing page with the drop zone and supported formats
-69%on the poster in the screenshot, WebP at quality 80, no visible loss
6codecs running in the browser, none on a server
0bytes uploaded, ever

Ask me about

  • Why cancelling stale encodes mattered more than which codec I picked
  • How the target-size search converges, and when it has to downscale instead
  • What the GPL decision cost and what it bought
  • What I found when I compared the shipped codec binaries with upstream

Built with Next.js, React, TypeScript, WebAssembly, Web Workers, MozJPEG, libwebp, libaom, libjxl, OxiPNG.

Next case study

HR portal

Open HR portal
Lakshay Agarwal, 2026Updated September 2026. Noida / Delhi NCR.