DEV Community

James Zhang
James Zhang

Posted on

No Uploads, No Servers: What I Learned Building Image Tools That Run 100% in Your Browser

No Uploads, No Servers: What I Learned Building Image Tools That Run 100% in Your Browser

Every online image tool I've ever used has the same flow: pick a file, wait for an upload, wait for processing, download the result. Somewhere in between, your photo — which might be a private document, a personal picture, or a client's asset — sits on someone else's server.

I kept wondering: why does any of this need a server at all?

Browsers today are absurdly capable. Canvas, WASM, Workers — you can do real image processing entirely on the client. So I built SnappyKit: a suite of image tools (compress, convert, crop, resize, watermark, HEIC conversion, and more) where every single byte of image data stays on your device. You can literally open DevTools, go to the Network tab, compress a 20MB photo, and watch zero upload requests fire.

In this post I'll share the architectural decisions and the gotchas that surprised me along the way.

The Core Architecture

The rule is simple and strict: image processing code lives in the browser, full stop.

The stack is Next.js 14 with React 18 and TypeScript. Pages are server components — but only for SEO metadata and layout. The actual tool logic lives in plain modules that only touch browser APIs:

  • The Canvas API handles the bread-and-butter operations: resizing, format conversion, compression, cropping.
  • WebAssembly fills the gap where Canvas can't go — most notably HEIC decoding (more on that below).
  • Everything is wired so that there is no code path where a file becomes a network request. Not a telemetry request, not a "preview" upload, nothing.

This constraint turned out to be the most useful design decision I made. It forced every feature to be answerable with one question: can this run on the client? If the answer was no, the feature didn't ship.

Gotcha #1: Canvas Has Real Limits

The first surprise: you cannot just draw any image onto a canvas and expect it to work.

Browsers impose hard limits on canvas dimensions — usually around 16,000px per side, with a total area cap somewhere around 268 megapixels (varies by engine). A modern phone shooting 50MP photos is fine, but a 100MP medium-format camera image will silently fail or produce a blank canvas.

So every tool enforces global limits up front: 20MB max file size, 100MP max total pixels, 16,000px max per side. If a file exceeds these, the user gets a friendly error before any processing starts — not a mysterious broken download afterward.

The lesson: validate before you touch the pixels. Your users should never discover browser limits by hitting them.

Gotcha #2: HEIC Is the Reason WASM Exists

Apple's HEIC format is the single most requested conversion on any image tool site. iPhone photos default to it, and half the Windows and web world can't read it.

No browser can natively decode HEIC. The only realistic path is compiling libheif to WebAssembly and lazy-loading it on the HEIC converter page only. A few notes from the trenches:

  • The WASM bundle is meaningful in size, so it loads on demand — users of the other 20 tools never pay the cost.
  • libheif's embind bindings need eval at runtime, which means the site's Content Security Policy needs a scoped exception (unsafe-eval) for exactly one route. Global CSP loosening was never on the table.
  • ESM/CJS interop between the WASM package and webpack needed its own build rule. One afternoon lost, never recovered.

The payoff is worth it: a .HEIC file goes in, a JPG comes out, and the whole decode happens in the user's browser with zero upload latency.

Gotcha #3: Browser Engines Disagree About AVIF

AVIF is the future of image compression, and browser support is genuinely good now. But "genuinely good" is not "identical."

Encoding an AVIF file through Canvas works smoothly in Chromium. Firefox and WebKit have their own levels of support, and the encode path in particular varies more than decode. If your tool promises AVIF output, you need feature detection per operation — not a single "does this browser support AVIF?" check.

The pragmatic approach: detect support at the operation level, and degrade gracefully. If AVIF encoding isn't available in the current engine, tell the user immediately and offer WebP instead — don't fail after they've already picked their settings.

Gotcha #4: "Free" Local Processing Still Needs Guardrails

Since nothing leaves the device, you'd think resource limits matter less. It's the opposite — a stuck browser tab is your product's fault.

Processing a huge image on the main thread freezes the UI, and there's no server timeout to bail you out. That's why the size caps exist, and why long operations give progress feedback. The browser is a hostile runtime with no supervisor; defensive limits are not optional.

What's Next

The roadmap is more tools built on the same foundation: more format pairs, smarter batch handling, and richer editing operations — all still 100% client-side. The privacy guarantee is the product, so it will never be a trade-off.

If you want to see the no-upload architecture in action, everything lives at snappykit.site — every tool is free, no account, and you can watch the Network tab stay empty the whole time.

If you're building something similar, I'd genuinely love to hear how you're handling the same edge cases. The comments are open.

Top comments (1)