Back to Blog
Web WorkersJavaScriptPerformanceMultithreading

Web Workers: Multithreading in JavaScript

Offload heavy computations to background threads. From dedicated workers to shared workers to service workers.

B
Bootspring Team
Engineering
July 12, 2024
5 min read

JavaScript is single-threaded, but Web Workers let you run scripts in background threads. Keep your UI responsive while handling heavy computations.

The Problem#

1// This blocks the UI 2function fibonacci(n) { 3 if (n <= 1) return n; 4 return fibonacci(n - 1) + fibonacci(n - 2); 5} 6 7// UI freezes during calculation 8const result = fibonacci(45); 9console.log(result);

Dedicated Workers#

Creating a Worker#

1// main.js 2const worker = new Worker('worker.js'); 3 4// Send message to worker 5worker.postMessage({ type: 'fibonacci', n: 45 }); 6 7// Receive results 8worker.onmessage = (event) => { 9 console.log('Result:', event.data.result); 10}; 11 12worker.onerror = (error) => { 13 console.error('Worker error:', error.message); 14};
1// worker.js 2function fibonacci(n) { 3 if (n <= 1) return n; 4 return fibonacci(n - 1) + fibonacci(n - 2); 5} 6 7self.onmessage = (event) => { 8 const { type, n } = event.data; 9 10 if (type === 'fibonacci') { 11 const result = fibonacci(n); 12 self.postMessage({ result }); 13 } 14};

Inline Workers#

1// Create worker from blob 2function createInlineWorker(fn) { 3 const blob = new Blob([`(${fn.toString()})()`], { 4 type: 'application/javascript', 5 }); 6 return new Worker(URL.createObjectURL(blob)); 7} 8 9const worker = createInlineWorker(() => { 10 self.onmessage = (e) => { 11 const result = e.data * 2; 12 self.postMessage(result); 13 }; 14}); 15 16worker.postMessage(21); 17worker.onmessage = (e) => console.log(e.data); // 42

Transferable Objects#

1// Transferring data (zero-copy) 2const buffer = new ArrayBuffer(1024 * 1024); // 1MB 3 4// Transfer ownership (fast, but buffer becomes unusable in main thread) 5worker.postMessage({ buffer }, [buffer]); 6console.log(buffer.byteLength); // 0 - transferred 7 8// In worker 9self.onmessage = (event) => { 10 const { buffer } = event.data; 11 const view = new Uint8Array(buffer); 12 13 // Process data 14 for (let i = 0; i < view.length; i++) { 15 view[i] = view[i] * 2; 16 } 17 18 // Transfer back 19 self.postMessage({ buffer }, [buffer]); 20};

Worker Pool#

1class WorkerPool { 2 private workers: Worker[] = []; 3 private queue: Array<{ 4 data: any; 5 resolve: (value: any) => void; 6 reject: (error: any) => void; 7 }> = []; 8 private availableWorkers: Worker[] = []; 9 10 constructor(workerScript: string, poolSize: number = navigator.hardwareConcurrency) { 11 for (let i = 0; i < poolSize; i++) { 12 const worker = new Worker(workerScript); 13 worker.onmessage = (event) => this.handleMessage(worker, event); 14 worker.onerror = (error) => this.handleError(worker, error); 15 this.workers.push(worker); 16 this.availableWorkers.push(worker); 17 } 18 } 19 20 execute<T>(data: any): Promise<T> { 21 return new Promise((resolve, reject) => { 22 const worker = this.availableWorkers.pop(); 23 24 if (worker) { 25 this.runTask(worker, data, resolve, reject); 26 } else { 27 this.queue.push({ data, resolve, reject }); 28 } 29 }); 30 } 31 32 private runTask( 33 worker: Worker, 34 data: any, 35 resolve: (value: any) => void, 36 reject: (error: any) => void 37 ) { 38 (worker as any)._resolve = resolve; 39 (worker as any)._reject = reject; 40 worker.postMessage(data); 41 } 42 43 private handleMessage(worker: Worker, event: MessageEvent) { 44 const resolve = (worker as any)._resolve; 45 resolve(event.data); 46 this.releaseWorker(worker); 47 } 48 49 private handleError(worker: Worker, error: ErrorEvent) { 50 const reject = (worker as any)._reject; 51 reject(error); 52 this.releaseWorker(worker); 53 } 54 55 private releaseWorker(worker: Worker) { 56 const next = this.queue.shift(); 57 if (next) { 58 this.runTask(worker, next.data, next.resolve, next.reject); 59 } else { 60 this.availableWorkers.push(worker); 61 } 62 } 63 64 terminate() { 65 this.workers.forEach((worker) => worker.terminate()); 66 } 67} 68 69// Usage 70const pool = new WorkerPool('compute-worker.js', 4); 71 72const results = await Promise.all([ 73 pool.execute({ task: 'hash', data: 'password1' }), 74 pool.execute({ task: 'hash', data: 'password2' }), 75 pool.execute({ task: 'hash', data: 'password3' }), 76]);

Shared Workers#

1// Shared across tabs/windows 2// shared-worker.js 3const connections = []; 4 5self.onconnect = (event) => { 6 const port = event.ports[0]; 7 connections.push(port); 8 9 port.onmessage = (e) => { 10 // Broadcast to all connections 11 connections.forEach((conn) => { 12 conn.postMessage({ 13 from: connections.indexOf(port), 14 message: e.data, 15 }); 16 }); 17 }; 18 19 port.start(); 20};
1// main.js (in multiple tabs) 2const worker = new SharedWorker('shared-worker.js'); 3 4worker.port.onmessage = (event) => { 5 console.log('Received:', event.data); 6}; 7 8worker.port.start(); 9worker.port.postMessage('Hello from this tab');
1// With Comlink library 2// worker.js 3import * as Comlink from 'comlink'; 4 5const api = { 6 async processImage(imageData) { 7 // Heavy processing 8 return processedData; 9 }, 10 11 async calculateStats(data) { 12 return { 13 mean: data.reduce((a, b) => a + b) / data.length, 14 // ... more calculations 15 }; 16 }, 17}; 18 19Comlink.expose(api);
1// main.js 2import * as Comlink from 'comlink'; 3 4const worker = new Worker('worker.js'); 5const api = Comlink.wrap(worker); 6 7// Call worker methods directly 8const stats = await api.calculateStats([1, 2, 3, 4, 5]); 9console.log(stats.mean);

Use Cases#

Good for Workers: ✓ Image/video processing ✓ Data parsing (CSV, JSON) ✓ Cryptographic operations ✓ Complex calculations ✓ Compression/decompression ✓ Syntax highlighting Not for Workers: ✗ DOM manipulation ✗ Simple calculations ✗ Tasks requiring window/document ✗ Short-lived operations

Limitations#

1// No access to: 2// - window 3// - document 4// - parent 5// - DOM APIs 6 7// Available in workers: 8// - fetch() 9// - IndexedDB 10// - WebSockets 11// - setTimeout/setInterval 12// - importScripts() 13// - navigator (partial) 14// - location (read-only)

Conclusion#

Web Workers enable true parallelism in JavaScript. Use them for computationally intensive tasks to keep your UI responsive.

The key is identifying operations that don't need DOM access and can benefit from parallel execution.

Share this article

Help spread the word about Bootspring