๐Ÿณ IntuitiveFE
Login
โ† All concepts

Web Workers

โฑ๏ธ ~3-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

๐Ÿ’ผ Web Worker = hiring an assistant to do the accounting while you keep talking to customers. You can't share your desk (no DOM access), but you can pass notes (postMessage).

The office analogy

You (main thread) handle the front desk โ€” talking to customers (DOM events), updating the display board (rendering), and running the register (JS execution). You can only do one thing at a time.

Your assistant (Web Worker) has their own desk in the back office. They can crunch numbers, process files, or run queries โ€” without interrupting you. They leave notes on your desk when done (postMessage).

The problem:your assistant can't come out front and update the display board themselves (no DOM access). Everything they produce must be handed to you via notes, and you update the display board.

Three types of workers

๐Ÿ‘ทWeb Worker

Per-page

CPU-intensive work: crypto, image processing, data parsing, prime sieves

๐Ÿ”—Shared Worker

Per-origin (all tabs)

Cross-tab state: shared WebSockets, real-time collaboration, cross-tab cache

๐Ÿ”งService Worker

Per-origin (network proxy)

Offline mode, background sync, push notifications, network interception

ASCII: main thread vs worker thread

js
1โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
2โ”‚ MAIN THREAD โ”‚ โ”‚ WORKER THREAD โ”‚
3โ”‚ โ”‚ โ”‚ โ”‚
4โ”‚ โœ“ DOM access โ”‚ โ”‚ โœ— No DOM access โ”‚
5โ”‚ โœ“ Window, document โ”‚ โ”‚ โœ— No window object โ”‚
6โ”‚ โœ“ LocalStorage โ”‚ โ”‚ โœ“ IndexedDB โ”‚
7โ”‚ โœ“ UI events โ”‚ โ”‚ โœ“ fetch, XHR โ”‚
8โ”‚ โœ“ Canvas (main) โ”‚ โ”‚ โœ“ WebSockets โ”‚
9โ”‚ โœ“ requestAnimationFrame โ”‚ โ”‚ โœ“ Math, crypto โ”‚
10โ”‚ โ”‚ โ”‚ โœ“ OffscreenCanvas โ”‚
11โ”‚ Event loop driven โ”‚ โ”‚ Event loop driven โ”‚
12โ”‚ (single-threaded) โ”‚ โ”‚ (separate OS thread) โ”‚
13โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
14 โ”‚ postMessage(data) โ”‚
15 โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚
16 โ”‚ โ”‚
17 โ”‚ postMessage(result) โ”‚
18 โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚
19 โ”‚ โ”‚
20 onmessage โ†’ update UI compute / process

The structured clone algorithm

postMessage serializes data using the structured clone algorithm โ€” a superset of JSON. Supports: primitives, Arrays, Objects, Date, Map, Set, RegExp, ArrayBuffer, Blob, ImageData. Does NOT support: functions, DOM nodes, or class instances with methods.

๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Worker type

Heavy computation moved off main thread. postMessage/onmessage for communication.

Main thread: 60fpsโ€” main thread stays responsive during all worker operations

Message timeline

0msmainpostMessage({type:'compute',n:1000000})
50msworker// computing primes...
2400msworkerpostMessage({primes:[...],count:78498})
2401msmainonmessage โ†’ update UI
ts
1// worker.js
2self.onmessage = function({ data }) {
3 if (data.type === 'compute') {
4 const primes = findPrimes(data.n);
5 self.postMessage({ primes, count: primes.length });
6 }
7};
8ย 
9function findPrimes(n) {
10 const sieve = new Uint8Array(n + 1);
11 const primes = [];
12 for (let i = 2; i <= n; i++) {
13 if (!sieve[i]) {
14 primes.push(i);
15 for (let j = i * i; j <= n; j += i) sieve[j] = 1;
16 }
17 }
18 return primes;
19}
โš ๏ธ Gotcha: Workers cannot access the DOM. Any UI updates must be posted back to the main thread.
๐Ÿ’ก Insight: Web Workers are true parallel execution โ€” they run on a separate OS thread, not the event loop. Heavy CPU work (image processing, crypto, data parsing) belongs here.
Viewed:๐Ÿ‘ท๐Ÿ”—๐Ÿ”งโš›๏ธ๐ŸŽจ
๐ŸŽฏ

Challenge

View all 5 worker types. For each, check both the worker code and main code tabs โ€” the communication pattern is different for each type.

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Exact interview questions

Q: Why can't Web Workers access the DOM?

The DOM is not thread-safe. If two threads could modify the DOM simultaneously, you'd need locking โ€” and lock contention would negate the performance gains. The browser spec made a deliberate decision: workers get a clean execution environment (no DOM, no window) but true parallelism. All DOM updates must be routed through the main thread.

js
1// โœ— Inside worker โ€” throws ReferenceError
2document.getElementById('output').textContent = result;
3ย 
4// โœ“ Post result to main thread, update DOM there
5self.postMessage({ result });
6// In main: worker.onmessage = ({data}) => el.textContent = data.result

Q: What is the difference between Web Worker, Shared Worker, and Service Worker?

Web Worker: dedicated to one page/tab. Terminated when the page closes. Use for heavy computation.

Shared Worker: one instance shared across all tabs on the same origin. Persists as long as any tab has a reference. Use for cross-tab communication without a server.

Service Worker: a network proxy that persists independently of any page. Acts as an offline-capable middleware between your app and the network. Use for PWA features: caching, push notifications, background sync.

js
1// Web Worker โ€” one per tab
2const w = new Worker('./heavy-task.js');
3ย 
4// Shared Worker โ€” one per origin
5const sw = new SharedWorker('./shared-state.js');
6sw.port.start();
7sw.port.postMessage('hello');
8ย 
9// Service Worker โ€” registered at origin level
10navigator.serviceWorker.register('/sw.js');

Q: What are Transferable objects?

Transferable objects are moved between threads rather than copied. The source loses access โ€” the ArrayBuffer is detached in the sender and available in the receiver. Zero-copy: no memory duplication for large buffers.

Transferable types: ArrayBuffer, MessagePort, OffscreenCanvas, ReadableStream, WritableStream, TransformStream, ImageBitmap, VideoFrame.

js
1// Transfer (move, not copy) โ€” source is detached
2const buffer = new ArrayBuffer(1024 * 1024); // 1MB
3worker.postMessage({ buffer }, [buffer]); // [buffer] = transfer list
4console.log(buffer.byteLength); // 0 โ€” detached!
5ย 
6// Copy (default) โ€” both sides have a copy
7worker.postMessage({ buffer }); // no transfer list = clone
8console.log(buffer.byteLength); // 1048576 โ€” still valid

Production patterns

Pattern: Comlink โ€” make workers feel like async functions

Comlink (by Google Chrome Labs) wraps postMessage/onmessage behind a Proxy, making worker calls look like async function calls. No manual message routing.

js
1// worker.js
2import { expose } from 'comlink';
3const api = {
4 async computePrimes(n) { return findPrimes(n); }
5};
6expose(api);
7ย 
8// main.js
9import { wrap } from 'comlink';
10const worker = new Worker('./worker.js');
11const api = wrap(worker);
12const primes = await api.computePrimes(1_000_000);
13// No postMessage/onmessage โ€” just async/await!

Pattern: Worker pool for parallel processing

For maximum throughput, create a pool of workers equal to navigator.hardwareConcurrency (number of CPU cores). Distribute tasks across the pool and collect results.

js
1const POOL_SIZE = navigator.hardwareConcurrency || 4;
2const pool = Array.from({ length: POOL_SIZE },
3 () => new Worker('./task-worker.js')
4);
5let poolIndex = 0;
6ย 
7function runInPool(data) {
8 return new Promise(resolve => {
9 const worker = pool[poolIndex++ % POOL_SIZE];
10 worker.onmessage = ({ data }) => resolve(data);
11 worker.postMessage(data);
12 });
13}
14ย 
15// Process 1000 items across all CPU cores
16const results = await Promise.all(items.map(runInPool));
๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

Structured clone algorithm โ€” what it supports

TypeSupportedNotes
Primitives (string, number, bool)โœ“Copied by value
Date, RegExpโœ“Reconstructed
Array, Object, Map, Setโœ“Deep cloned
ArrayBuffer, TypedArrayโœ“Cloned or transferred
Blob, File, ImageDataโœ“Cloned
Error objectsโœ“message + stack preserved
Functionsโœ—Not serializable
DOM nodesโœ—Worker has no DOM
Class instances (methods)โœ—Only own enumerable properties
Symbolโœ—Not supported

SharedArrayBuffer security requirements (COOP/COEP)

SharedArrayBuffer was disabled in all browsers in 2018 after Spectre. It was re-enabled in 2020 only for cross-origin isolated pages. Your server must send these headers:

js
1# Required response headers
2Cross-Origin-Opener-Policy: same-origin
3Cross-Origin-Embedder-Policy: require-corp
4ย 
5# Check isolation in JS
6console.log(self.crossOriginIsolated); // must be true
7ย 
8# In Next.js (next.config.js)
9headers: async () => [{
10 source: '/(.*)',
11 headers: [
12 { key: 'Cross-Origin-Opener-Policy', value: 'same-origin' },
13 { key: 'Cross-Origin-Embedder-Policy', value: 'require-corp' },
14 ],
15}]

Cross-origin isolation prevents embedding third-party content (iframes, images from other origins) unless they opt in with Cross-Origin-Resource-Policy: cross-origin.

Comlink library

Comlink (1.1KB gzipped) wraps the postMessage protocol behind ES6 Proxy, making cross-thread calls look like normal async function calls. Under the hood: every call is assigned a nonce, tracked in a Map, and resolved when the corresponding response arrives.

js
1// worker.js
2import * as Comlink from 'comlink';
3Comlink.expose({
4 processImage: async (buffer) => {
5 const view = new Uint8Array(buffer);
6 // ... heavy processing ...
7 return processedBuffer;
8 }
9});
10ย 
11// main.js
12import * as Comlink from 'comlink';
13const worker = new Worker('./worker.js');
14const api = Comlink.wrap(worker);
15ย 
16// Feels like a local async call โ€” runs in worker
17const result = await api.processImage(
18 Comlink.transfer(buffer, [buffer])
19);

Worker threads in Node.js

Node.js has the worker_threads module (stable since v12). Similar API to browser Workers but with direct SharedArrayBuffer support (no COOP/COEP needed server-side) and workerData for initialization.

js
1import { Worker, isMainThread, workerData, parentPort }
2 from 'worker_threads';
3ย 
4if (isMainThread) {
5 const worker = new Worker(__filename, {
6 workerData: { n: 1_000_000 }
7 });
8 worker.on('message', result => console.log(result));
9} else {
10 // Running in worker thread
11 const primes = findPrimes(workerData.n);
12 parentPort.postMessage(primes.length);
13}

WASM + Workers combo

WebAssembly runs inside Workers โ€” this is the ultimate performance pattern. Offload both the JS-to-native performance boundary (WASM) AND the main-thread blocking problem (Worker) simultaneously.

js
1// wasm-worker.js
2import initWasm, { processData } from './pkg/my_wasm.js';
3ย 
4let wasmReady = initWasm(); // initialize WASM module
5ย 
6self.onmessage = async ({ data }) => {
7 await wasmReady; // ensure WASM is loaded
8 const result = processData(data.input); // WASM call in worker
9 self.postMessage(result);
10};
11ย 
12// main.js
13const worker = new Worker('./wasm-worker.js');
14worker.postMessage({ input: largeDataset });
15worker.onmessage = ({ data }) => renderResults(data);
16// Main thread: 0ms blocking. WASM in worker: full CPU.
๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

The DOM is not thread-safe. Concurrent mutation would require locking, negating the parallelism benefit.

Structured clone is a deep-copy algorithm used by postMessage. It handles most JS types but not functions, DOM nodes, or class methods.

Transferables are moved (zero-copy) between threads rather than copied โ€” the source is detached after transfer.

COOP: same-origin and COEP: require-corp are required. They enforce cross-origin isolation to mitigate Spectre-class timing attacks.

Shared Worker when you need one worker shared across multiple tabs โ€” e.g., a WebSocket connection, cross-tab state sync, or shared computation cache.

Pool size = navigator.hardwareConcurrency (CPU logical cores). Distribute tasks round-robin or by availability and collect results with Promises.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

What is the BroadcastChannel API and how does it differ from SharedWorker for cross-tab communication?