Web Workers
Web Workers run JavaScript in a separate OS thread, parallel to the main thread. They enable CPU-intensive work without freezing the UI — but they cannot access the DOM and communicate exclusively through message passing.
Why Use Workers?
The main thread handles rendering, user input, and JavaScript execution. Long-running synchronous code blocks all of these:
// Blocks UI for seconds — BAD on main thread
function crunchData(data) {
return data.map(heavyComputation);
}
Workers move this work off the main thread:
const worker = new Worker('worker.js');
worker.postMessage(largeDataset);
worker.onmessage = (e) => updateUI(e.data);
Dedicated Worker — Basic Pattern
main.js
const worker = new Worker('worker.js');
worker.postMessage({ numbers: [1, 2, 3, 4, 5] });
worker.onmessage = (e) => {
console.log('Result:', e.data); // 15
};
worker.onerror = (e) => {
console.error('Worker error:', e.message, 'at', e.filename, e.lineno);
};
// Clean up when done
worker.terminate();
worker.js
self.onmessage = (e) => {
const sum = e.data.numbers.reduce((a, b) => a + b, 0);
self.postMessage(sum);
};
Workers have their own global scope (self or globalThis) — no access to window, document, or parent variables.
Inline Worker with Blob URL
Create a worker without a separate file:
const workerCode = `
self.onmessage = (e) => {
const result = e.data * 2;
self.postMessage(result);
};
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.postMessage(21);
worker.onmessage = (e) => console.log(e.data); // 42
// Revoke blob URL to free memory
URL.revokeObjectURL(blob);
Useful for dynamically generated worker logic or bundler-inlined workers.
Transferable Objects
Transfer ArrayBuffer ownership for zero-copy performance — the sender loses access:
const buffer = new ArrayBuffer(1024 * 1024); // 1 MB
fillBuffer(buffer);
worker.postMessage(buffer, [buffer]);
// buffer.byteLength is now 0 in main thread — transferred, not copied
Transferable types: ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas.
Structured Clone Algorithm
Messages are copied using the structured clone algorithm. Supported: objects, arrays, typed arrays, Map, Set, Date, RegExp. Not supported: functions, DOM nodes, symbols.
worker.postMessage({
matrix: new Float32Array(1000),
config: { iterations: 100 },
lookup: new Map([['a', 1]])
});
Module Workers (ES Modules)
Modern browsers support ES module workers:
const worker = new Worker('worker.js', { type: 'module' });
worker.js
import { processData } from './utils.js';
self.onmessage = (e) => {
self.postMessage(processData(e.data));
};
SharedWorker
Shared across multiple tabs, windows, and iframes from the same origin:
const worker = new SharedWorker('shared-worker.js');
worker.port.start();
worker.port.postMessage({ action: 'increment' });
worker.port.onmessage = (e) => {
console.log('Shared count:', e.data.count);
};
Use for shared state like WebSocket connections or cross-tab coordination.
Service Workers
Special workers for offline caching, push notifications, and background sync (Progressive Web Apps):
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered with scope:', registration.scope);
})
.catch(error => {
console.error('SW registration failed:', error);
});
}
Service workers act as network proxies — intercepting fetch requests for cache strategies.
Practical Example: Image Processing
main.js
const worker = new Worker('image-worker.js');
document.querySelector('#file').addEventListener('change', async (e) => {
const file = e.target.files[0];
const buffer = await file.arrayBuffer();
worker.postMessage({ buffer, width: 800 }, [buffer]);
});
worker.onmessage = (e) => {
const blob = new Blob([e.data.buffer], { type: 'image/png' });
document.querySelector('#preview').src = URL.createObjectURL(blob);
};
Use Cases
| Task | Why Worker Helps |
|---|---|
| Image/video processing | Pixel manipulation is CPU-heavy |
| Large JSON/CSV parsing | Avoid blocking during parse |
| Cryptographic hashing | CPU-intensive, security-sensitive |
| Data compression | gzip/brotli on large payloads |
| Scientific simulation | Long computation loops |
| Real-time analytics | Aggregate without UI jank |
Limitations
- No DOM access — cannot read or modify the page directly
- No shared memory by default — message passing only (SharedArrayBuffer requires cross-origin isolation headers)
- Same-origin policy — worker script must be same origin (or CORS-enabled)
- Startup overhead — not worth it for trivial computations (< ~50ms)
- Debugging is harder — separate DevTools context for worker threads
Best Practices
- Batch messages — avoid sending thousands of small messages; aggregate data.
- Transfer large buffers instead of cloning when ownership can move.
- Terminate workers when done to free thread resources.
- Handle errors with
onerrorand try/catch inside the worker. - Feature-detect before creating workers:
if (typeof Worker !== 'undefined'). - Pool workers for repeated tasks instead of creating one per operation.
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| Worker fails to load | Wrong path or CORS | Check script URL; verify same origin |
postMessage silently fails |
Non-cloneable data (function, DOM node) | Send plain data objects only |
| Main thread still slow | Worker creation overhead or message churn | Pool workers; batch operations |
| SharedArrayBuffer unavailable | Missing COOP/COEP headers | Set cross-origin isolation headers |
| Memory leak | Worker never terminated | Call worker.terminate() when done |
Web Workers are the primary tool for keeping JavaScript applications responsive when CPU-bound work would otherwise block the event loop and freeze the user interface.