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 onerror and 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.