Web Workers enable multi-threaded JavaScript. Here's how to use them for heavy computation without blocking the UI.
Basic Web Worker#
1// worker.js
2self.onmessage = function(event) {
3 const { data } = event;
4
5 // Heavy computation
6 const result = processData(data);
7
8 // Send result back
9 self.postMessage(result);
10};
11
12function processData(data) {
13 // CPU-intensive work
14 let result = 0;
15 for (let i = 0; i < data.iterations; i++) {
16 result += Math.sqrt(i);
17 }
18 return result;
19}
20
21// main.js
22const worker = new Worker('worker.js');
23
24worker.onmessage = function(event) {
25 console.log('Result:', event.data);
26};
27
28worker.onerror = function(error) {
29 console.error('Worker error:', error);
30};
31
32worker.postMessage({ iterations: 1000000 });TypeScript Web Worker#
1// worker.ts
2interface WorkerMessage {
3 type: 'process' | 'cancel';
4 payload?: any;
5}
6
7interface WorkerResponse {
8 type: 'result' | 'progress' | 'error';
9 data: any;
10}
11
12self.onmessage = (event: MessageEvent<WorkerMessage>) => {
13 const { type, payload } = event.data;
14
15 switch (type) {
16 case 'process':
17 try {
18 const result = heavyComputation(payload);
19 self.postMessage({ type: 'result', data: result } as WorkerResponse);
20 } catch (error) {
21 self.postMessage({
22 type: 'error',
23 data: (error as Error).message,
24 } as WorkerResponse);
25 }
26 break;
27
28 case 'cancel':
29 // Handle cancellation
30 break;
31 }
32};
33
34function heavyComputation(data: any): any {
35 // Report progress
36 for (let i = 0; i <= 100; i += 10) {
37 self.postMessage({ type: 'progress', data: i } as WorkerResponse);
38 }
39
40 return processedResult;
41}
42
43// Export empty to make it a module
44export {};1// useWorker.ts
2import { useEffect, useRef, useState, useCallback } from 'react';
3
4interface UseWorkerOptions<T, R> {
5 onResult?: (result: R) => void;
6 onProgress?: (progress: number) => void;
7 onError?: (error: string) => void;
8}
9
10function useWorker<T, R>(
11 workerFactory: () => Worker,
12 options: UseWorkerOptions<T, R> = {}
13) {
14 const workerRef = useRef<Worker | null>(null);
15 const [isProcessing, setIsProcessing] = useState(false);
16 const [progress, setProgress] = useState(0);
17
18 useEffect(() => {
19 workerRef.current = workerFactory();
20
21 workerRef.current.onmessage = (event) => {
22 const { type, data } = event.data;
23
24 switch (type) {
25 case 'result':
26 setIsProcessing(false);
27 options.onResult?.(data);
28 break;
29 case 'progress':
30 setProgress(data);
31 options.onProgress?.(data);
32 break;
33 case 'error':
34 setIsProcessing(false);
35 options.onError?.(data);
36 break;
37 }
38 };
39
40 return () => {
41 workerRef.current?.terminate();
42 };
43 }, [workerFactory]);
44
45 const process = useCallback((data: T) => {
46 setIsProcessing(true);
47 setProgress(0);
48 workerRef.current?.postMessage({ type: 'process', payload: data });
49 }, []);
50
51 const cancel = useCallback(() => {
52 workerRef.current?.postMessage({ type: 'cancel' });
53 setIsProcessing(false);
54 }, []);
55
56 return { process, cancel, isProcessing, progress };
57}
58
59// Usage
60function DataProcessor() {
61 const [result, setResult] = useState(null);
62
63 const { process, isProcessing, progress } = useWorker<InputData, OutputData>(
64 () => new Worker(new URL('./worker.ts', import.meta.url)),
65 {
66 onResult: setResult,
67 onError: (error) => console.error(error),
68 }
69 );
70
71 return (
72 <div>
73 <button onClick={() => process(inputData)} disabled={isProcessing}>
74 Process
75 </button>
76 {isProcessing && <progress value={progress} max={100} />}
77 {result && <Results data={result} />}
78 </div>
79 );
80}Transferable Objects#
1// Transfer ownership for better performance
2// Useful for large ArrayBuffers
3
4// worker.js
5self.onmessage = (event) => {
6 const buffer = event.data;
7
8 // Process the buffer
9 const result = new Uint8Array(buffer);
10 for (let i = 0; i < result.length; i++) {
11 result[i] = result[i] * 2;
12 }
13
14 // Transfer back (buffer is moved, not copied)
15 self.postMessage(result.buffer, [result.buffer]);
16};
17
18// main.js
19const buffer = new ArrayBuffer(1024 * 1024); // 1MB
20
21// Transfer ownership (buffer becomes unusable in main thread)
22worker.postMessage(buffer, [buffer]);
23
24// buffer.byteLength is now 0
25
26// Transferable types:
27// - ArrayBuffer
28// - MessagePort
29// - ImageBitmap
30// - OffscreenCanvasWorker Pool#
1class WorkerPool {
2 private workers: Worker[] = [];
3 private taskQueue: Array<{
4 data: any;
5 resolve: (value: any) => void;
6 reject: (error: Error) => void;
7 }> = [];
8 private availableWorkers: Worker[] = [];
9
10 constructor(
11 private workerScript: string,
12 private poolSize: number = navigator.hardwareConcurrency || 4
13 ) {
14 for (let i = 0; i < poolSize; i++) {
15 const worker = new Worker(workerScript);
16 this.workers.push(worker);
17 this.availableWorkers.push(worker);
18 }
19 }
20
21 exec<T>(data: any): Promise<T> {
22 return new Promise((resolve, reject) => {
23 const task = { data, resolve, reject };
24
25 if (this.availableWorkers.length > 0) {
26 this.runTask(task);
27 } else {
28 this.taskQueue.push(task);
29 }
30 });
31 }
32
33 private runTask(task: typeof this.taskQueue[0]) {
34 const worker = this.availableWorkers.pop()!;
35
36 worker.onmessage = (event) => {
37 task.resolve(event.data);
38 this.releaseWorker(worker);
39 };
40
41 worker.onerror = (error) => {
42 task.reject(new Error(error.message));
43 this.releaseWorker(worker);
44 };
45
46 worker.postMessage(task.data);
47 }
48
49 private releaseWorker(worker: Worker) {
50 this.availableWorkers.push(worker);
51
52 if (this.taskQueue.length > 0) {
53 const nextTask = this.taskQueue.shift()!;
54 this.runTask(nextTask);
55 }
56 }
57
58 terminate() {
59 this.workers.forEach((worker) => worker.terminate());
60 }
61}
62
63// Usage
64const pool = new WorkerPool('worker.js', 4);
65
66// Process multiple items in parallel
67const results = await Promise.all(
68 items.map((item) => pool.exec(item))
69);
70
71pool.terminate();Shared Worker#
1// shared-worker.js
2// Single worker shared across tabs/windows
3
4const connections: MessagePort[] = [];
5
6self.onconnect = (event: MessageEvent) => {
7 const port = event.ports[0];
8 connections.push(port);
9
10 port.onmessage = (event) => {
11 const { type, data } = event.data;
12
13 switch (type) {
14 case 'broadcast':
15 // Send to all connected tabs
16 connections.forEach((conn) => {
17 conn.postMessage({ type: 'message', data });
18 });
19 break;
20
21 case 'sync':
22 // Synchronize state across tabs
23 port.postMessage({ type: 'state', data: sharedState });
24 break;
25 }
26 };
27
28 port.start();
29};
30
31// main.js
32const sharedWorker = new SharedWorker('shared-worker.js');
33
34sharedWorker.port.onmessage = (event) => {
35 console.log('Received:', event.data);
36};
37
38sharedWorker.port.start();
39sharedWorker.port.postMessage({ type: 'broadcast', data: 'Hello from tab!' });Comlink for Easy Workers#
1// worker.ts with Comlink
2import * as Comlink from 'comlink';
3
4const api = {
5 async heavyComputation(data: number[]): Promise<number> {
6 // Simulate heavy work
7 await new Promise((r) => setTimeout(r, 1000));
8 return data.reduce((a, b) => a + b, 0);
9 },
10
11 processImage(imageData: ImageData): ImageData {
12 // Process image pixels
13 const data = imageData.data;
14 for (let i = 0; i < data.length; i += 4) {
15 // Grayscale conversion
16 const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
17 data[i] = data[i + 1] = data[i + 2] = avg;
18 }
19 return imageData;
20 },
21};
22
23Comlink.expose(api);
24
25// main.ts
26import * as Comlink from 'comlink';
27
28const worker = new Worker(new URL('./worker.ts', import.meta.url));
29const api = Comlink.wrap<typeof import('./worker').api>(worker);
30
31// Use like regular async functions
32const result = await api.heavyComputation([1, 2, 3, 4, 5]);
33console.log(result); // 15OffscreenCanvas#
1// Offload canvas rendering to worker
2// main.js
3const canvas = document.getElementById('canvas') as HTMLCanvasElement;
4const offscreen = canvas.transferControlToOffscreen();
5
6const worker = new Worker('canvas-worker.js');
7worker.postMessage({ canvas: offscreen }, [offscreen]);
8
9// canvas-worker.js
10let canvas: OffscreenCanvas;
11let ctx: OffscreenCanvasRenderingContext2D;
12
13self.onmessage = (event) => {
14 if (event.data.canvas) {
15 canvas = event.data.canvas;
16 ctx = canvas.getContext('2d')!;
17 render();
18 }
19};
20
21function render() {
22 // Render loop in worker
23 ctx.clearRect(0, 0, canvas.width, canvas.height);
24
25 // Draw complex graphics
26 for (let i = 0; i < 1000; i++) {
27 ctx.fillRect(
28 Math.random() * canvas.width,
29 Math.random() * canvas.height,
30 10,
31 10
32 );
33 }
34
35 requestAnimationFrame(render);
36}Best Practices#
When to Use:
✓ Heavy computations (sorting, parsing)
✓ Image/video processing
✓ Data transformation
✓ Complex calculations
When NOT to Use:
✗ Simple operations
✗ DOM manipulation (not allowed)
✗ Small data sets
✗ Frequent small messages
Performance:
✓ Use transferable objects
✓ Minimize message passing
✓ Pool workers for many tasks
✓ Terminate unused workers
Communication:
✓ Structure messages clearly
✓ Handle errors properly
✓ Consider using Comlink
✓ Batch small messages
Conclusion#
Web Workers enable parallel processing in JavaScript without blocking the main thread. Use them for CPU-intensive tasks, leverage transferable objects for large data, and consider worker pools for concurrent processing. Libraries like Comlink simplify the communication API significantly.