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-removal is 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.toBlob on Chrome for Android sometimes waited about 4 seconds before returning in my tests. Using toDataURL there 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.