ImagePDF.Tools
Technical

Building a Private Image Compressor with WebAssembly

N
NikolaLast updated on June 20, 2026 · 10 min read

Summary

How ImagePDF.Tools uses pngquant WASM, OffscreenCanvas, and Web Workers to compress images entirely in the browser, zero server uploads, full privacy.

The engineering challenge behind a privacy-first image tool: how do you give users professional compression quality without sending a single byte to a server? Every competing approach either uploads your file or sacrifices compression quality to avoid it.

The answer is WebAssembly (WASM), native C binaries compiled to run inside the browser sandbox at near-native speed. Paired with the browser's own OffscreenCanvas API and Web Workers, WASM enables professional-grade compression that runs entirely on the user's device, with zero network requests carrying file data.

This is a technical walkthrough of how ImagePDF.Tools achieves this using three distinct paths: pngquant WASM for PNG lossy quantisation, the Canvas API for JPEG and WebP encoding, and a Web Worker architecture that keeps the UI at 60fps regardless of file size.

WebAssembly client-side processing: file stays in browser memory, goes to WASM module, never to server | ImagePDF.Tools
WebAssembly runs native-speed compression algorithms inside the browser sandbox. Your file never leaves your device.

Three Compression Paths, Zero Uploads

Different image formats require different compression codecs. The architecture uses the right tool for each format:

Output FormatCompression EngineBrowser APIWASM Required?
JPEGCanvas JPEG encoder (libjpeg-derived)OffscreenCanvas.convertToBlob()No
WebPChrome/Safari native WebP encoderOffscreenCanvas.convertToBlob()No
PNG (lossy)pngquant colour quantisationWASM + FileReaderYes (~350 KB)
PNG (lossless)Browser PNG encoderOffscreenCanvas.convertToBlob()No
Compression method by output format

The key insight: browsers already ship JPEG and WebP encoders as part of the Canvas API. No additional code is needed for those formats. PNG is the outlier, Canvas PNG output is always lossless, so meaningful lossy PNG compression requires a WASM-compiled codec.

PNG Compression with pngquant WASM

pngquant is the gold-standard lossy PNG compressor, the same algorithm used by TinyPNG, Squoosh, and ImageOptim. It reduces PNG colours through palette quantisation (similar to GIF's approach but at much higher quality), then applies lossless DEFLATE compression to the quantised output. Typical savings: 60–80% file size reduction with no perceptible quality loss on photographic PNGs.

typescript
// lib/pngquant.ts
let pngquantModule: Awaited<ReturnType<typeof initPngquant>> | null = null;

async function getPngquant() {
  if (!pngquantModule) {
    // WASM is loaded and compiled once per session, cached in memory
    const { default: initPngquant } = await import('@nicolo-ribaudo/pngquant-wasm');
    pngquantModule = await initPngquant();
  }
  return pngquantModule;
}

export async function compressPng(file: File, quality: number): Promise<File> {
  const pngquant = await getPngquant();
  const input = new Uint8Array(await file.arrayBuffer()); // stays in browser memory

  // quality=80 → [72, 80] gives a tight quality band
  // speed: 1 = slowest/best quality, 11 = fastest/worst
  const output = pngquant.compress(input, {
    quality: [Math.max(0, quality * 0.9), quality] as [number, number],
    speed: 3,
  });

  return new File([output], file.name, { type: 'image/png' });
}

JPEG and WebP via OffscreenCanvas

For JPEG and WebP, the browser's own encoder does the work. OffscreenCanvas is the key: it is a canvas that can be created and operated in a Web Worker, with no DOM access needed. This means the entire encode pipeline runs off the main thread.

typescript
// lib/compress.ts, runs inside a Web Worker (no DOM access)
export async function compressJpegOrWebp(
  file: File,
  quality: number,
  outputFormat: 'image/jpeg' | 'image/webp',
  maxDimension = 4096,
): Promise<File> {
  const bitmap = await createImageBitmap(file);

  // Scale down if image exceeds maxDimension on either axis
  const scale = Math.min(1, maxDimension / Math.max(bitmap.width, bitmap.height));
  const w = Math.round(bitmap.width * scale);
  const h = Math.round(bitmap.height * scale);

  // OffscreenCanvas: no DOM, no main-thread, works in Web Workers
  const canvas = new OffscreenCanvas(w, h);
  const ctx = canvas.getContext('2d')!;
  ctx.drawImage(bitmap, 0, 0, w, h);
  bitmap.close(); // release GPU memory immediately

  const blob = await canvas.convertToBlob({ type: outputFormat, quality: quality / 100 });
  const ext = outputFormat === 'image/jpeg' ? 'jpg' : 'webp';
  return new File([blob], file.name.replace(/\.[^.]+$/, '.' + ext), { type: outputFormat });
}

Web Worker Architecture: 60fps During Compression

Compressing a 12MP photo takes 200ms–2s depending on format and device. Running this on the main JavaScript thread would freeze the UI for the entire duration, scroll would stall, animations would drop frames, the interface would feel broken.

Web Workers solve this: they run in a separate OS thread with access to OffscreenCanvas, WASM modules, and ArrayBuffer transfers, but no DOM access. The UI thread dispatches a file and options; the Worker processes it and returns the compressed buffer. The UI remains fully interactive throughout.

typescript
// workers/compress.worker.ts
self.onmessage = async ({ data: { file, options, id } }) => {
  try {
    const result = options.outputFormat === 'image/png'
      ? await compressPng(file, options.quality)
      : await compressJpegOrWebp(file, options.quality, options.outputFormat);

    const buf = await result.arrayBuffer();
    // Transferable ArrayBuffer: zero-copy, ownership passes to main thread
    self.postMessage({ id, buf, name: result.name, type: result.type }, [buf]);
  } catch (err) {
    self.postMessage({ id, error: (err as Error).message });
  }
};

The Transferable pattern in the postMessage call is critical for performance. Without it, the ArrayBuffer is copied from the Worker's memory to the main thread's memory, doubling peak memory usage for large files. With the transfer, ownership moves instantly at zero cost.

WASM Loading Strategy

The pngquant WASM binary is ~350 KB, small enough to be worth prefetching, large enough that a cache miss on first PNG compression would cause a noticeable delay. The loading strategy:

  1. 1.Prefetch on page load using <link rel="prefetch">, the binary is downloaded silently in the background after the page is interactive.
  2. 2.Lazy-import on first PNG drop, the WASM module is not imported until a PNG file is actually needed, keeping the initial JS bundle at minimum size.
  3. 3.Singleton caching, after first initialization, the compiled WASM module is held in module scope. Subsequent PNG compressions skip the compile step entirely.
💡

Add <link rel="prefetch" href="/_next/static/chunks/pngquant_bg.wasm"> to your document head. The browser downloads it at idle priority, it's ready before the user drops a PNG, with no impact on LCP or initial load.

Performance Benchmarks

FormatFile SizeM3 MacBookMid-range AndroidNotes
JPEG quality 8012MP (8 MB raw)180–350 ms400–900 msHardware-accelerated in Chrome
WebP quality 8012MP (8 MB raw)120–280 ms350–800 msFastest, native browser encoder
PNG via pngquant5MP (4 MB raw)800ms–2 s2–5 spngquant is CPU-intensive by design
WASM compile (1st visit)-~200 ms~400 msCached after first run; <5 ms on return
Compression performance by format and device

Verifying Zero Uploads

The privacy claim is verifiable. To confirm no file data leaves your browser:

  1. 1.Open any tool on ImagePDF.Tools
  2. 2.Open Chrome DevTools (F12) → Network tab → filter by "Fetch/XHR"
  3. 3.Drop any file into the tool
  4. 4.Watch the Network tab during processing, zero XHR/fetch requests will carry your file data

The only network requests you'll see are the initial page load, the Clerk session check (an authentication token, not file data), and for free-tier users, the AdSense script. Your file bytes never appear in any network request.

The Bottom Line

WASM-powered client-side compression is not a compromise, it delivers the same quality as server-side tools while providing absolute privacy guarantees. The architecture (OffscreenCanvas + WASM + Web Worker + Transferable ArrayBuffers) is the combination that makes it possible without any user-perceptible performance cost.

Frequently asked questions

Does browser-based compression give the same quality as server-side tools?
Yes. The pngquant algorithm used for PNG compression is the same library used by TinyPNG, Squoosh, and ImageOptim, running in WASM is functionally identical to running it natively. For JPEG and WebP, the browser's native encoder quality is comparable to libjpeg-turbo at equivalent settings.
What is WebAssembly (WASM) and why is it used?
WebAssembly is a binary instruction format that runs in the browser at near-native speed. It allows native C/C++ codecs like pngquant to be compiled once and run in any modern browser without installation. The result is professional-grade compression running entirely on the user's device.
How does the tool process files if nothing is uploaded?
When you drop a file, it is read into browser memory as an ArrayBuffer. The WASM module or OffscreenCanvas encoder processes those bytes in-memory, and the output is another ArrayBuffer that the browser lets you save. No network request is made with the file data at any point.
What is OffscreenCanvas?
OffscreenCanvas is a Web API that creates a canvas element not attached to the DOM, which can be used inside Web Workers. This allows image encoding (JPEG, WebP) to happen off the main JavaScript thread, keeping the UI responsive during compression.
Are there file size limitations for browser-based compression?
Browsers cap JavaScript heap size, typically at 1.5–4 GB depending on device and browser. Files larger than ~100 MB may exceed this limit on lower-memory devices. ImagePDF.Tools enforces a 100 MB file size limit to ensure reliable processing across device types.

Sources & references

This article was researched and written by Nikola, drawing on the following primary sources and documentation:

Ready to try it?

All tools run entirely in your browser, no uploads, no account required.

Compress Image
You're offline, cached tools still work