Use Web Workers for Heavy Computation Without Freezing UI
Move CPU-heavy tasks off the main thread with Web Workers and message passing — best practices, fallbacks, and performance considerations.
Advertisement
Question
You need to run expensive image-processing computations (filters, resizing) in the browser without freezing the UI. Design a solution using Web Workers. Include error handling, transferable objects, and fallback strategy.
Answer
Web Workers provide a way to run scripts in background threads. They can perform CPU-bound tasks without blocking rendering. For performance-sensitive code (image processing, crypto, large array computation), workers are essential. Use transferable objects (ArrayBuffer) for zero-copy transfers, chunking for large datasets, and structured message protocols to keep code maintainable.
SEO & UX considerations:
- Offloading work reduces jank -> better Core Web Vitals (CLS, FID).
- Use fallback to chunked
requestIdleCallbackfor environments without workers. - Use worker pools for parallelism when multiple CPU cores are beneficial.
Implementation
Main thread (main.js):
// create worker
const worker = new Worker('/workers/imageWorker.js');
worker.onmessage = (e) => {
const { type, payload } = e.data;
if (type === 'result') {
// payload could be an ImageBitmap or ArrayBuffer
renderResult(payload);
} else if (type === 'error') {
console.error('Worker error:', payload);
}
};
function processImage(arrayBuffer) {
// Transfer the buffer to the worker (zero-copy)
worker.postMessage({ type: 'process', payload: arrayBuffer }, [arrayBuffer]);
}
Worker (workers/imageWorker.js):
self.onmessage = async (e) => {
try {
const { type, payload } = e.data;
if (type === 'process') {
// payload is an ArrayBuffer
const result = doHeavyWork(new Uint8ClampedArray(payload));
// Transfer result back
self.postMessage({ type: 'result', payload: result.buffer }, [
result.buffer,
]);
}
} catch (err) {
self.postMessage({ type: 'error', payload: err.message });
}
};
function doHeavyWork(pixels) {
// example: invert image colors (CPU-heavy for big images)
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = 255 - pixels[i];
pixels[i + 1] = 255 - pixels[i + 1];
pixels[i + 2] = 255 - pixels[i + 2];
}
return pixels;
}
Fallback (no Web Worker):
// chunked processing on main thread using requestIdleCallback
function processChunked(pixels) {
let i = 0;
function work(deadline) {
while (deadline.timeRemaining() > 0 && i < pixels.length) {
// process some pixels
i += 4000;
}
if (i < pixels.length) requestIdleCallback(work);
else renderResult(pixels);
}
requestIdleCallback(work);
}
Visualization
sequenceDiagram participant UI participant Main participant Worker UI->>Main: User uploads image Main->>Worker: postMessage(ArrayBuffer) Worker->>Worker: process pixels (no UI lock) Worker-->>Main: postMessage(result ArrayBuffer) Main->>UI: render processed image
Real-World Example & Tips
- Use
createImageBitmap()andOffscreenCanvas(in workers) for efficient canvas operations. - For browsers supporting
OffscreenCanvas, you can draw directly in a worker. - Limit worker creation per CPU core; use a worker pool for many parallel jobs.
- Use transferable objects for large typed arrays to avoid copy overhead.
Quick Practice
- Build a worker that computes the sum of a 10M-length Float32Array and returns the result.
- Measure UI responsiveness (FPS) with and without the worker.
Summary
Web Workers unlock background computation, enabling smooth UIs. Combine transferable objects, worker pools, and graceful fallbacks for production-grade performance.
Can Web Workers access the DOM?
No — workers run in a separate global scope and can’t access window/document directly. Use message passing.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement