DEV Community

Cover image for Efficient Client-Side Background Removal with WebAssembly and JavaScript
NasajTools
NasajTools

Posted on

Efficient Client-Side Background Removal with WebAssembly and JavaScript

As frontend architects, we often face a "Buy vs. Build" dilemma that usually morphs into a "Server vs. Client" debate. When building the image processing suite for NasajTools, specifically the background remover, I hit a wall with the traditional server-side approach.

Processing high-resolution images on a backend requires significant CPU/GPU resources. It introduces latency (upload + process + download), costs money for every compute cycle, and raises privacy concerns for users who hesitate to upload personal photos to a cloud black box.

I decided to shift the workload entirely to the browser. This post details how we implemented client-side background removal using WebAssembly (Wasm) and JavaScript, effectively reducing our server costs to near zero while improving user privacy.

The Problem: Latency and The Main Thread
The challenge with image segmentation (separating the foreground from the background) is that it is computationally expensive.

If you run a heavy segmentation model directly on the main JavaScript thread, the UI freezes. The browser becomes unresponsive, animations stutter, and the user experience degrades immediately. Furthermore, loading the necessary neural network models (often 10MB+) can slow down the initial page load if not handled correctly.

We needed a solution that:

Runs locally (no API calls for processing).

Does not block the main UI thread.

Handles high-resolution images without crashing the browser tab.

The Solution: Offloading to Web Workers
To solve this, we utilized the @imgly/background-removal library, which leverages ONNX Runtime Web and WebAssembly to run models efficiently in the browser. However, simply importing the library isn't enough for a production-grade tool.

We had to architect a robust wrapper around the library using Web Workers. This ensures that the heavy lifting happens on a background thread, leaving the main thread free to handle UI updates (like progress bars or drag-and-drop interactions).

The Code
Here is the core logic for setting up the background removal service. We encapsulate the removal logic to handle the blob conversions and ensure clean garbage collection.

// background-removal.worker.js
import imglyRemoveBackground from "@imgly/background-removal";

self.onmessage = async (event) => {
  const { imageBlob, config } = event.data;

  try {
    // Notify main thread: Processing started
    self.postMessage({ type: 'STATUS', payload: 'Processing...' });

    // The core removal logic
    // We pass a config object to fine-tune the model (e.g., debug mode, model size)
    const blob = await imglyRemoveBackground(imageBlob, {
      progress: (key, current, total) => {
        const percentage = Math.round((current / total) * 100);
        self.postMessage({ type: 'PROGRESS', payload: percentage });
      },
      debug: false,
      model: "medium", // Balancing speed vs. quality
      ...config
    });

    // Send the result back to the main thread
    self.postMessage({ type: 'SUCCESS', payload: blob });

  } catch (error) {
    self.postMessage({ type: 'ERROR', payload: error.message });
  }
};
Enter fullscreen mode Exit fullscreen mode

And here is how we consume this worker in our main React component (simplified for clarity):

// ImageUploader.jsx
import { useEffect, useRef, useState } from 'react';

const ImageUploader = () => {
  const workerRef = useRef(null);
  const [processedImage, setProcessedImage] = useState(null);
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    // Initialize the worker
    workerRef.current = new Worker(new URL('./background-removal.worker.js', import.meta.url));

    workerRef.current.onmessage = (event) => {
      const { type, payload } = event.data;

      switch (type) {
        case 'PROGRESS':
          setProgress(payload);
          break;
        case 'SUCCESS':
          // Create a local URL for the processed blob to display it immediately
          const url = URL.createObjectURL(payload);
          setProcessedImage(url);
          break;
        case 'ERROR':
          console.error("Worker Error:", payload);
          break;
      }
    };

    return () => workerRef.current.terminate();
  }, []);

  const handleProcess = (file) => {
    // Offload the file to the worker immediately
    workerRef.current.postMessage({ imageBlob: file });
  };

  return (
    <div>
      {/* UI Code for Dropzone */}
      {progress > 0 && <progress value={progress} max="100" />}
      {processedImage && <img src={processedImage} alt="Background Removed" />}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Live Demo
You can see this implementation running in production. We use this exact worker pattern to handle drag-and-drop processing seamlessly.

👉 Try it here:https://nasajtools.com/tools/image/remove-background

Performance Considerations
While the Web Worker keeps the UI responsive, we ran into a few specific "gotchas" during development that you should be aware of:

  1. Caching the Model
    The AI model files are static assets. We configured our Service Worker to cache the .onnx and .wasm files aggressively. This means that after the first usage, the tool works offline and loads almost instantly. If you don't cache these, the user burns 20MB+ of data every time they refresh the page.

  2. Memory Leaks with Blobs
    When you generate an image URL using URL.createObjectURL(blob), the browser keeps that data in memory until the document is unloaded or you manually release it. In a Single Page Application (SPA), this is a memory leak waiting to happen.

We implemented a cleanup routine using the useEffect cleanup function:

useEffect(() => {
  return () => {
    if (processedImage) {
      URL.revokeObjectURL(processedImage);
    }
  };
}, [processedImage]);
Enter fullscreen mode Exit fullscreen mode
  1. Fallbacks WebAssembly support is excellent in modern browsers, but occasionally, older devices or strict corporate firewalls might block Wasm execution or the download of binary model files. We implemented a basic error boundary that alerts the user if their environment doesn't support the required features, saving them from silently failing interactions.

Summary
Moving background removal to the client side was a massive win for NasajTools. It reduced our server infrastructure complexity, improved privacy for our users, and provided a snappy interface that doesn't depend on internet speed for processing.

If you are building image manipulation tools today, I highly recommend looking into WebAssembly solutions before defaulting to a Python backend.

Top comments (0)