DEV Community

toolsvale
toolsvale

Posted on

How I Built an Image Converter That Never Touches a Server

How I Built an Image Converter That Never Touches a Server

Home - ToolsVale

favicon toolsvale.com

Most "free online image converter" sites work the same way under the hood: your file gets POSTed to an endpoint, a server does the actual conversion, and you get a download link back. That's normal and fine for a lot of use cases — but it means every file you convert briefly exists on infrastructure you don't control, and it means the tool needs a backend, storage, cleanup jobs, and rate limits just to exist.

I wanted to see if I could skip all of that and do the conversion entirely in the browser instead. Turns out modern browser APIs make this pretty viable. Here's roughly how it works.

The core idea: Canvas as a conversion engine

The browser's element can decode most image formats it can display, and re-encode them via canvas.toBlob() or canvas.toDataURL() with a different MIME type. So a JPG → WebP conversion is roughly:

async function convertImage(file, outputType = 'image/webp', quality = 0.8) {
const bitmap = await createImageBitmap(file);
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
const blob = await canvas.convertToBlob({ type: outputType, quality });
return blob;
}

createImageBitmap handles the decode off the main thread, OffscreenCanvas lets you do this inside a Web Worker (important — you don't want to block the UI on a 40-image batch), and convertToBlob handles re-encoding. No network request anywhere in that flow.

Where it gets harder: formats the browser can't natively decode

Canvas only helps for formats the browser already knows how to render. HEIC is the obvious problem — no mainstream browser decodes HEIC natively, since it's patent-encumbered and Apple-specific. For that, you need a WASM-compiled decoder (libheif compiled to WASM works) running client-side, which decodes HEIC into raw pixel data the canvas can then take over from. Same category of problem for AVIF on older browser versions, though support there is improving fast.

The general pattern: whatever the browser can decode natively, let it. Whatever it can't, ship a WASM decoder for. This keeps the bundle smaller than compiling everything to WASM, since you're only covering the gaps.

The trade-offs, honestly
Performance is your user's device, not yours. A batch of 200 large images on a low-end laptop will be slow. There's no way around this — you're trading server cost for user CPU time. For most day-to-day batch sizes it's a non-issue, but it's not free.
No persistence. Nothing's stored anywhere, which is the whole point, but it also means there's no "history" or account-based re-download. Close the tab before saving and it's gone.
Web Workers are non-negotiable for batches. Running conversion on the main thread freezes the UI on anything more than a couple of files. OffscreenCanvas + Workers is what makes multi-file drag-and-drop feel responsive instead of janky.
Memory matters more than you'd think. Holding decoded bitmaps for dozens of large images in memory simultaneously adds up fast — worth converting and releasing one at a time rather than decoding everything upfront.
Why bother, when server-side is simpler to build

Two reasons I found compelling enough to go this route:

No file-size or file-count caps to justify. Server-side tools cap free usage because storage and bandwidth cost money. Client-side has no such constraint — the only limit is the user's own machine.
It's verifiable, not just claimed. "We don't store your files" is a common claim on converter sites, but users can't check it. With client-side processing, anyone can open the network tab or go offline mid-conversion and see for themselves that nothing left the device.

I ended up building this out into a small set of tools — ToolsVale — covering the common image conversion pairs (JPG/PNG/HEIC/AVIF ↔ WebP), image-to-PDF, and compression, all running on this pattern.

Curious if anyone else here has shipped something similar — particularly interested in how others have handled HEIC decode performance, since that's been the trickiest part so far.

Top comments (0)