Web Workers¶
True parallelism in the browser using separate threads.
When to Use Web Workers¶
- CPU-intensive tasks — Image processing, data parsing, calculations
- Keep UI responsive — Offload heavy work from main thread
- Background processing — Tasks that don't need immediate UI feedback
Not for: - Simple async operations (use promises) - DOM manipulation (workers can't access DOM) - Quick computations (overhead > benefit)
Basic Web Worker¶
Main Script¶
// main.js
const worker = new Worker('worker.js');
// Send data to worker
worker.postMessage({ data: [1, 2, 3, 4, 5], operation: 'sum' });
// Receive results
worker.onmessage = (event) => {
console.log('Result:', event.data);
};
// Handle errors
worker.onerror = (error) => {
console.error('Worker error:', error.message);
};
// Terminate when done
worker.terminate();
Worker Script¶
// worker.js
self.onmessage = (event) => {
const { data, operation } = event.data;
let result;
switch (operation) {
case 'sum':
result = data.reduce((a, b) => a + b, 0);
break;
case 'sort':
result = [...data].sort((a, b) => a - b);
break;
default:
result = null;
}
// Send result back
self.postMessage(result);
};
Inline Workers¶
Create workers without separate files.
function createWorker(fn: () => void): Worker {
const blob = new Blob(
[`(${fn.toString()})()`],
{ type: 'application/javascript' }
);
return new Worker(URL.createObjectURL(blob));
}
// Usage
const worker = createWorker(() => {
self.onmessage = (e) => {
const result = e.data.map((x: number) => x * 2);
self.postMessage(result);
};
});
worker.postMessage([1, 2, 3]);
worker.onmessage = (e) => console.log(e.data); // [2, 4, 6]
Worker with TypeScript¶
Types¶
// worker.types.ts
export interface WorkerMessage {
type: 'PROCESS' | 'CANCEL';
payload: any;
}
export interface WorkerResponse {
type: 'RESULT' | 'ERROR' | 'PROGRESS';
payload: any;
}
Worker¶
// worker.ts
import type { WorkerMessage, WorkerResponse } from './worker.types';
self.onmessage = (event: MessageEvent<WorkerMessage>) => {
const { type, payload } = event.data;
try {
switch (type) {
case 'PROCESS':
const result = processData(payload);
const response: WorkerResponse = { type: 'RESULT', payload: result };
self.postMessage(response);
break;
case 'CANCEL':
// Handle cancellation
break;
}
} catch (error) {
const errorResponse: WorkerResponse = {
type: 'ERROR',
payload: error instanceof Error ? error.message : 'Unknown error',
};
self.postMessage(errorResponse);
}
};
function processData(data: number[]): number {
return data.reduce((sum, n) => sum + n * n, 0);
}
Main¶
// main.ts
import type { WorkerMessage, WorkerResponse } from './worker.types';
class TypedWorker {
private worker: Worker;
constructor() {
this.worker = new Worker(new URL('./worker.ts', import.meta.url));
}
process(data: number[]): Promise<number> {
return new Promise((resolve, reject) => {
const handler = (event: MessageEvent<WorkerResponse>) => {
const { type, payload } = event.data;
this.worker.removeEventListener('message', handler);
if (type === 'RESULT') {
resolve(payload);
} else if (type === 'ERROR') {
reject(new Error(payload));
}
};
this.worker.addEventListener('message', handler);
const message: WorkerMessage = { type: 'PROCESS', payload: data };
this.worker.postMessage(message);
});
}
terminate() {
this.worker.terminate();
}
}
// Usage
const worker = new TypedWorker();
const result = await worker.process([1, 2, 3, 4, 5]);
console.log(result); // 55
Comlink¶
Simplify worker communication with Comlink.
Worker¶
// worker.ts
import * as Comlink from 'comlink';
const api = {
async processData(data: number[]): Promise<number> {
return data.reduce((sum, n) => sum + n * n, 0);
},
async sortLargeArray(arr: number[]): Promise<number[]> {
return [...arr].sort((a, b) => a - b);
},
};
Comlink.expose(api);
Main¶
// main.ts
import * as Comlink from 'comlink';
const worker = new Worker(new URL('./worker.ts', import.meta.url));
const api = Comlink.wrap<typeof import('./worker')['api']>(worker);
// Call methods like regular async functions
const result = await api.processData([1, 2, 3, 4, 5]);
console.log(result); // 55
const sorted = await api.sortLargeArray([5, 2, 8, 1, 9]);
console.log(sorted); // [1, 2, 5, 8, 9]
Transferable Objects¶
Transfer ownership instead of copying for better performance.
// main.js
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
// Bad: Copies the buffer (slow)
worker.postMessage({ buffer });
// Good: Transfers ownership (fast)
worker.postMessage({ buffer }, [buffer]);
// buffer is now unusable in main thread
// worker.js
self.onmessage = (event) => {
const { buffer } = event.data;
// buffer is now owned by worker
// Process...
const result = new ArrayBuffer(1024);
// Transfer back
self.postMessage({ result }, [result]);
};
Transferable Types¶
- ArrayBuffer
- MessagePort
- ImageBitmap
- OffscreenCanvas
Shared Memory¶
SharedArrayBuffer allows shared memory between threads.
// main.js
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);
worker.postMessage({ sharedArray });
// Both main thread and worker can access sharedArray
sharedArray[0] = 42;
// worker.js
self.onmessage = (event) => {
const { sharedArray } = event.data;
// Atomic operations for thread safety
Atomics.add(sharedArray, 0, 1);
const value = Atomics.load(sharedArray, 0);
console.log(value); // 43
};
Atomics¶
// Thread-safe operations
Atomics.add(array, index, value);
Atomics.sub(array, index, value);
Atomics.load(array, index);
Atomics.store(array, index, value);
Atomics.exchange(array, index, value);
Atomics.compareExchange(array, index, expected, replacement);
// Synchronization
Atomics.wait(array, index, value, timeout);
Atomics.notify(array, index, count);
Worker Pool¶
Manage multiple workers for parallel processing.
class WorkerPool {
private workers: Worker[] = [];
private queue: Array<{
data: any;
resolve: (value: any) => void;
reject: (error: Error) => void;
}> = [];
private busyWorkers = new Set<Worker>();
constructor(workerScript: string, poolSize: number) {
for (let i = 0; i < poolSize; i++) {
const worker = new Worker(workerScript);
this.workers.push(worker);
worker.onmessage = (event) => {
this.busyWorkers.delete(worker);
this.processQueue();
};
}
}
async execute<T>(data: any): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push({ data, resolve, reject });
this.processQueue();
});
}
private processQueue() {
const availableWorker = this.workers.find(w => !this.busyWorkers.has(w));
const task = this.queue.shift();
if (availableWorker && task) {
this.busyWorkers.add(availableWorker);
const handler = (event: MessageEvent) => {
availableWorker.removeEventListener('message', handler);
task.resolve(event.data);
};
availableWorker.addEventListener('message', handler);
availableWorker.postMessage(task.data);
}
}
terminate() {
this.workers.forEach(w => w.terminate());
}
}
// Usage
const pool = new WorkerPool('worker.js', 4);
const results = await Promise.all(
items.map(item => pool.execute(item))
);
React Integration¶
useWorker Hook¶
import { useCallback, useEffect, useRef, useState } from 'react';
function useWorker<T, R>(workerFactory: () => Worker) {
const workerRef = useRef<Worker | null>(null);
const [result, setResult] = useState<R | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
workerRef.current = workerFactory();
workerRef.current.onmessage = (event: MessageEvent<R>) => {
setResult(event.data);
setLoading(false);
};
workerRef.current.onerror = (err) => {
setError(new Error(err.message));
setLoading(false);
};
return () => {
workerRef.current?.terminate();
};
}, [workerFactory]);
const run = useCallback((data: T) => {
setLoading(true);
setError(null);
workerRef.current?.postMessage(data);
}, []);
return { result, loading, error, run };
}
// Usage
function Component() {
const { result, loading, error, run } = useWorker<number[], number>(
() => new Worker(new URL('./worker.ts', import.meta.url))
);
return (
<button onClick={() => run([1, 2, 3, 4, 5])} disabled={loading}>
{loading ? 'Processing...' : `Result: ${result}`}
</button>
);
}
Best Practices¶
- Use for heavy computation — Don't over-engineer simple tasks
- Transfer, don't copy — Use Transferable for large data
- Pool workers — Reuse workers to avoid creation overhead
- Handle errors — Workers fail silently without error handlers
- Terminate when done — Clean up workers to free resources
- Consider Comlink — Simplifies worker communication
- Test worker logic — Extract pure functions for testing
Limitations¶
- No DOM access — Workers can't manipulate the DOM
- Limited APIs — No window, document, parent
- Serialization cost — postMessage serializes data
- Same-origin — Worker scripts must be same-origin
- Module support varies — Check browser compatibility