I'm building HD Resizer, a free set of image tools that run entirely in the browser: resize, compress, convert, PDF, background removal and
a few more. Your photos never leave your device. This post is about the background remover, because that's where I learned the most.
Step 1: pick a model you're allowed to use
Most "client-side background removal" libraries I found came with a catch:
-
@imgly/background-removalis AGPL-3.0. - Bria RMBG-2.0 is non-commercial.
I want to be able to monetize the site later, so I used two Apache-2.0 models instead:
- Best mode (products, pets, general photos): IS-Net (DIS "general use"), int8-quantised, 44 MB.
- People mode (portraits): MODNet, 26 MB.
If you ship something similar, check the licence of the weights and the upstream project, not only the code that loads them.
Step 2: run it with ONNX Runtime Web
I use onnxruntime-web with the WebAssembly backend. It runs single-threaded, because multi-threaded WASM needs cross-origin isolation headers (COOP/COEP), and
those can break embeds and third-party scripts. I didn't want to pay that price.
The models are plain static files on my own hosting, and they are loaded only when someone first uses the tool. A SHA-256 check in the build script makes sure the
files I deploy are the ones I tested.
Step 3: pre- and post-processing
- IS-Net: the photo is resized to 1024×1024 (that's how the model was trained), normalised, and run through the model. The output is rescaled to the range 0 to
- MODNet: shorter side 512, both sides rounded to a multiple of 32, long side capped at 1024.
- Applying the mask: I don't cut the small image. I scale the mask up and apply it to the full-resolution original with
globalCompositeOperation = 'destination-in'. So a 12 MP photo gives a 12 MP cutout, even though the model only saw 1024 px.
ctx.drawImage(original, 0, 0); ctx.globalCompositeOperation = 'destination-in'; ctx.imageSmoothingQuality = 'high'; ctx.drawImage(mask, 0, 0, mask.width, mask.height, 0, 0, original.width, original.height); What bit me
- Download size is the real UX cost. 44 MB is fine on Wi-Fi and painful on mobile data. I show progress, and offer the smaller 26 MB person-only mode for portraits.
- Hair and fur. Every automatic model struggles with fine edges. I added an Erase and Restore brush that works on a full-size copy of the cutout. It fixed most of the complaints.
-
canvas.toBlobon Chrome for Android sometimes waited about 4 seconds before returning in my tests. UsingtoDataURLthere was much faster. I only measured this on my own devices, so test yours. - Memory on phones. Big photos plus a model plus a canvas can run a phone out of memory. Keeping the model input small and doing the full-size work in one step helped.
What I'd do differently
- Try WebGPU as an optional faster path, with WASM as the fallback.
- Look at smaller distilled models for low-end phones.
** Try it**
It's free, with no sign-up and no watermark: hdresizer.com/remove-background.
I'd like to hear how it does on your photos, especially hard ones (hair, glass, shadows), and whether you've found a good permissively licensed model I missed.

