Your React app is grinding to a halt. The culprit? A massive data transformation, complex calculation, or image processing task that’s blocking the main thread. While your JavaScript crunches numbers, your UI freezes, animations stutter, and users rage-click unresponsive buttons. The solution isn’t to optimize the algorithm (though you should do that too)—it’s to move it off the main thread entirely with Web Workers.
Web Workers let you run JavaScript in background threads, parallel to your main execution thread. This means your expensive computations can run without blocking user interactions, keeping your React app responsive even during heavy processing. But integrating Workers with React’s component model requires careful handling of state synchronization, lifecycle management, and TypeScript typing. This guide shows you exactly how to do it right.
Understanding Web Workers in the Browser
Before diving into React integration, understand what Workers can and can’t do:
// Web Worker capabilities and limitations
interface WorkerCapabilities {
// What Workers CAN do
can: {
compute: 'Run CPU-intensive calculations';
fetch: 'Make network requests independently';
indexedDB: 'Access browser storage';
webAssembly: 'Run WASM modules for max performance';
postMessage: 'Communicate with main thread';
importScripts: 'Load additional JavaScript files';
};
// What Workers CANNOT do
cannot: {
dom: 'Access or manipulate the DOM';
window: 'Access window object or globals';
localStorage: 'Use localStorage (use IndexedDB instead)';
parent: 'Access parent object directly';
react: 'Use React components or hooks directly';
};
// Performance characteristics
performance: {
overhead: '~0.5-1ms to spawn a worker';
messageLatency: '~0.1-0.3ms for postMessage';
memory: 'Separate heap, typically 10-50MB baseline';
concurrency: 'True parallel execution on multi-core CPUs';
};
}Basic Web Worker Setup with React
Let’s start with a simple Worker integration:
Creating a TypeScript Worker
// worker/calculator.worker.ts
// This file runs in the Worker thread
type WorkerMessage =
| { type: 'CALCULATE_PRIMES'; limit: number }
| { type: 'PROCESS_DATA'; data: number[] }
| { type: 'CANCEL_OPERATION' };
type WorkerResponse =
| { type: 'RESULT'; data: any }
| { type: 'PROGRESS'; progress: number }
| { type: 'ERROR'; error: string };
// Worker context is different from window
const ctx: Worker = self as any;
// State within the worker
let isProcessing = false;
let shouldCancel = false;
// Calculate prime numbers (expensive operation)
function calculatePrimes(limit: number): number[] {
const primes: number[] = [];
for (let num = 2; num <= limit; num++) {
if (shouldCancel) {
throw new Error('Operation cancelled');
}
let isPrime = true;
for (let i = 2; i * i <= num; i++) {
if (num % i === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
primes.push(num);
}
// Send progress updates
if (num % 1000 === 0) {
ctx.postMessage({
type: 'PROGRESS',
progress: (num / limit) * 100,
} as WorkerResponse);
}
}
return primes;
}
// Process large dataset
function processData(data: number[]): number[] {
// Simulate expensive processing
return data.map((value, index) => {
// Report progress for large datasets
if (index % 1000 === 0) {
ctx.postMessage({
type: 'PROGRESS',
progress: (index / data.length) * 100,
} as WorkerResponse);
}
// Some expensive computation
return Math.sqrt(value) * Math.log(value + 1);
});
}
// Listen for messages from main thread
ctx.addEventListener('message', async (event: MessageEvent<WorkerMessage>) => {
const { type } = event.data;
if (type === 'CANCEL_OPERATION') {
shouldCancel = true;
return;
}
if (isProcessing) {
ctx.postMessage({
type: 'ERROR',
error: 'Worker is busy',
} as WorkerResponse);
return;
}
isProcessing = true;
shouldCancel = false;
try {
let result: any;
switch (type) {
case 'CALCULATE_PRIMES':
result = calculatePrimes(event.data.limit);
break;
case 'PROCESS_DATA':
result = processData(event.data.data);
break;
default:
throw new Error(`Unknown message type: ${type}`);
}
ctx.postMessage({
type: 'RESULT',
data: result,
} as WorkerResponse);
} catch (error) {
ctx.postMessage({
type: 'ERROR',
error: error instanceof Error ? error.message : 'Unknown error',
} as WorkerResponse);
} finally {
isProcessing = false;
shouldCancel = false;
}
});
// Signal that worker is ready
ctx.postMessage({ type: 'READY' });React Hook for Worker Management
// hooks/useWebWorker.ts
import { useEffect, useRef, useState, useCallback } from 'react';
interface UseWebWorkerOptions {
onProgress?: (progress: number) => void;
onError?: (error: string) => void;
terminateOnUnmount?: boolean;
}
export function useWebWorker<T = any>(workerPath: string, options: UseWebWorkerOptions = {}) {
const { onProgress, onError, terminateOnUnmount = true } = options;
const workerRef = useRef<Worker | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const [progress, setProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
// Initialize worker
useEffect(() => {
// Create worker with proper typing
workerRef.current = new Worker(new URL(workerPath, import.meta.url), { type: 'module' });
const worker = workerRef.current;
// Set up message handler
worker.onmessage = (event: MessageEvent) => {
const { type, data, error: errorMsg, progress: prog } = event.data;
switch (type) {
case 'READY':
console.log('Worker ready');
break;
case 'PROGRESS':
setProgress(prog);
onProgress?.(prog);
break;
case 'ERROR':
setError(errorMsg);
onError?.(errorMsg);
setIsProcessing(false);
break;
case 'RESULT':
// Results handled by promise resolution
setIsProcessing(false);
setProgress(100);
break;
}
};
worker.onerror = (event: ErrorEvent) => {
const errorMsg = `Worker error: ${event.message}`;
setError(errorMsg);
onError?.(errorMsg);
setIsProcessing(false);
};
// Cleanup
return () => {
if (terminateOnUnmount && worker) {
worker.terminate();
}
};
}, [workerPath, onProgress, onError, terminateOnUnmount]);
// Send message to worker and get result
const postMessage = useCallback(<R = T,>(message: any): Promise<R> => {
return new Promise((resolve, reject) => {
if (!workerRef.current) {
reject(new Error('Worker not initialized'));
return;
}
setIsProcessing(true);
setError(null);
setProgress(0);
const worker = workerRef.current;
// One-time result handler
const handleResult = (event: MessageEvent) => {
if (event.data.type === 'RESULT') {
worker.removeEventListener('message', handleResult);
resolve(event.data.data);
} else if (event.data.type === 'ERROR') {
worker.removeEventListener('message', handleResult);
reject(new Error(event.data.error));
}
};
worker.addEventListener('message', handleResult);
worker.postMessage(message);
});
}, []);
// Cancel ongoing operation
const cancel = useCallback(() => {
if (workerRef.current && isProcessing) {
workerRef.current.postMessage({ type: 'CANCEL_OPERATION' });
setIsProcessing(false);
setProgress(0);
}
}, [isProcessing]);
// Terminate worker
const terminate = useCallback(() => {
if (workerRef.current) {
workerRef.current.terminate();
workerRef.current = null;
setIsProcessing(false);
setProgress(0);
}
}, []);
return {
postMessage,
cancel,
terminate,
isProcessing,
progress,
error,
};
}Using the Worker in a Component
// components/PrimeCalculator.tsx
function PrimeCalculator() {
const [limit, setLimit] = useState(100000);
const [results, setResults] = useState<number[]>([]);
const { postMessage, cancel, isProcessing, progress, error } = useWebWorker(
'../worker/calculator.worker.ts',
);
const calculatePrimes = async () => {
try {
const primes = await postMessage<number[]>({
type: 'CALCULATE_PRIMES',
limit,
});
setResults(primes);
} catch (err) {
console.error('Failed to calculate primes:', err);
}
};
return (
<div className="prime-calculator">
<h2>Prime Number Calculator</h2>
<div className="controls">
<input
type="number"
value={limit}
onChange={(e) => setLimit(Number(e.target.value))}
disabled={isProcessing}
placeholder="Enter limit"
/>
<button onClick={calculatePrimes} disabled={isProcessing}>
{isProcessing ? 'Calculating...' : 'Calculate Primes'}
</button>
{isProcessing && <button onClick={cancel}>Cancel</button>}
</div>
{isProcessing && (
<div className="progress">
<div className="progress-bar" style={{ width: `${progress}%` }} />
<span>{progress.toFixed(1)}%</span>
</div>
)}
{error && <div className="error">Error: {error}</div>}
{results.length > 0 && (
<div className="results">
<h3>Found {results.length} prime numbers</h3>
<div className="prime-list">
{results.slice(0, 100).join(', ')}
{results.length > 100 && '...'}
</div>
</div>
)}
</div>
);
}Advanced Worker Patterns
Worker Pool for Parallel Processing
// utils/workerPool.ts
export class WorkerPool<T = any> {
private workers: Worker[] = [];
private queue: Array<{
data: any;
resolve: (value: T) => void;
reject: (error: any) => void;
}> = [];
private busyWorkers = new Set<Worker>();
constructor(
private workerPath: string,
private poolSize: number = navigator.hardwareConcurrency || 4,
) {
this.initializePool();
}
private initializePool() {
for (let i = 0; i < this.poolSize; i++) {
const worker = new Worker(new URL(this.workerPath, import.meta.url), { type: 'module' });
worker.onmessage = (event) => {
this.handleWorkerMessage(worker, event);
};
worker.onerror = (error) => {
console.error('Worker error:', error);
this.busyWorkers.delete(worker);
this.processQueue();
};
this.workers.push(worker);
}
}
private handleWorkerMessage(worker: Worker, event: MessageEvent) {
const { type, data, error } = event.data;
// Get the task associated with this worker
const task = (worker as any).__currentTask;
if (type === 'RESULT') {
task?.resolve(data);
} else if (type === 'ERROR') {
task?.reject(new Error(error));
}
// Mark worker as available
this.busyWorkers.delete(worker);
delete (worker as any).__currentTask;
// Process next item in queue
this.processQueue();
}
private processQueue() {
if (this.queue.length === 0) return;
// Find available worker
const availableWorker = this.workers.find((w) => !this.busyWorkers.has(w));
if (!availableWorker) return;
// Get next task from queue
const task = this.queue.shift();
if (!task) return;
// Assign task to worker
this.busyWorkers.add(availableWorker);
(availableWorker as any).__currentTask = task;
availableWorker.postMessage(task.data);
}
async process(data: any): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push({ data, resolve, reject });
this.processQueue();
});
}
async processAll(items: any[]): Promise<T[]> {
return Promise.all(items.map((item) => this.process(item)));
}
terminate() {
this.workers.forEach((worker) => worker.terminate());
this.workers = [];
this.queue = [];
this.busyWorkers.clear();
}
getStats() {
return {
poolSize: this.poolSize,
busyWorkers: this.busyWorkers.size,
queueLength: this.queue.length,
utilization: (this.busyWorkers.size / this.poolSize) * 100,
};
}
}
// Hook for using worker pool
export function useWorkerPool<T = any>(workerPath: string, poolSize?: number) {
const poolRef = useRef<WorkerPool<T> | null>(null);
const [stats, setStats] = useState({
poolSize: 0,
busyWorkers: 0,
queueLength: 0,
utilization: 0,
});
useEffect(() => {
poolRef.current = new WorkerPool<T>(workerPath, poolSize);
// Update stats periodically
const interval = setInterval(() => {
if (poolRef.current) {
setStats(poolRef.current.getStats());
}
}, 100);
return () => {
clearInterval(interval);
poolRef.current?.terminate();
};
}, [workerPath, poolSize]);
const process = useCallback(async (data: any): Promise<T> => {
if (!poolRef.current) {
throw new Error('Worker pool not initialized');
}
return poolRef.current.process(data);
}, []);
const processAll = useCallback(async (items: any[]): Promise<T[]> => {
if (!poolRef.current) {
throw new Error('Worker pool not initialized');
}
return poolRef.current.processAll(items);
}, []);
return {
process,
processAll,
stats,
};
}Transferable Objects for Large Data
// utils/transferableWorker.ts
// Worker that handles large binary data efficiently
// worker/imageProcessor.worker.ts
const ctx: Worker = self as any;
interface ImageProcessingMessage {
type: 'PROCESS_IMAGE';
imageData: ImageData;
filter: 'blur' | 'sharpen' | 'grayscale';
}
function processImage(imageData: ImageData, filter: string): ImageData {
const pixels = imageData.data;
const width = imageData.width;
const height = imageData.height;
// Create new ImageData for result
const output = new ImageData(width, height);
const outputPixels = output.data;
switch (filter) {
case 'grayscale':
for (let i = 0; i < pixels.length; i += 4) {
const gray = pixels[i] * 0.299 + pixels[i + 1] * 0.587 + pixels[i + 2] * 0.114;
outputPixels[i] = gray; // R
outputPixels[i + 1] = gray; // G
outputPixels[i + 2] = gray; // B
outputPixels[i + 3] = pixels[i + 3]; // A
}
break;
case 'blur':
// Simple box blur
const blurRadius = 2;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let r = 0,
g = 0,
b = 0,
a = 0;
let count = 0;
for (let dy = -blurRadius; dy <= blurRadius; dy++) {
for (let dx = -blurRadius; dx <= blurRadius; dx++) {
const ny = y + dy;
const nx = x + dx;
if (ny >= 0 && ny < height && nx >= 0 && nx < width) {
const idx = (ny * width + nx) * 4;
r += pixels[idx];
g += pixels[idx + 1];
b += pixels[idx + 2];
a += pixels[idx + 3];
count++;
}
}
}
const idx = (y * width + x) * 4;
outputPixels[idx] = r / count;
outputPixels[idx + 1] = g / count;
outputPixels[idx + 2] = b / count;
outputPixels[idx + 3] = a / count;
}
}
break;
// Add more filters as needed
}
return output;
}
ctx.addEventListener('message', (event: MessageEvent<ImageProcessingMessage>) => {
const { type, imageData, filter } = event.data;
if (type === 'PROCESS_IMAGE') {
const result = processImage(imageData, filter);
// Transfer the result back (zero-copy transfer)
ctx.postMessage(
{ type: 'RESULT', imageData: result },
[result.data.buffer], // Transfer ownership of the buffer
);
}
});
// Hook for efficient data transfer
export function useTransferableWorker(workerPath: string) {
const workerRef = useRef<Worker | null>(null);
useEffect(() => {
workerRef.current = new Worker(new URL(workerPath, import.meta.url), { type: 'module' });
return () => {
workerRef.current?.terminate();
};
}, [workerPath]);
const processWithTransfer = useCallback(
async <T = any,>(message: any, transferables: Transferable[]): Promise<T> => {
return new Promise((resolve, reject) => {
if (!workerRef.current) {
reject(new Error('Worker not initialized'));
return;
}
const worker = workerRef.current;
const handleMessage = (event: MessageEvent) => {
if (event.data.type === 'RESULT') {
worker.removeEventListener('message', handleMessage);
resolve(event.data);
} else if (event.data.type === 'ERROR') {
worker.removeEventListener('message', handleMessage);
reject(new Error(event.data.error));
}
};
worker.addEventListener('message', handleMessage);
// Transfer ownership of buffers to worker (zero-copy)
worker.postMessage(message, transferables);
});
},
[],
);
return { processWithTransfer };
}SharedArrayBuffer for Real-Time Communication
// utils/sharedWorker.ts
// Note: SharedArrayBuffer requires specific headers for security
export class SharedMemoryWorker {
private worker: Worker;
private sharedBuffer: SharedArrayBuffer;
private sharedArray: Int32Array;
constructor(workerPath: string, bufferSize: number = 1024) {
// Create shared memory
this.sharedBuffer = new SharedArrayBuffer(bufferSize * 4); // 4 bytes per int32
this.sharedArray = new Int32Array(this.sharedBuffer);
// Create worker with shared memory
this.worker = new Worker(new URL(workerPath, import.meta.url), { type: 'module' });
// Send shared buffer to worker
this.worker.postMessage({
type: 'INIT_SHARED_MEMORY',
buffer: this.sharedBuffer,
});
}
// Atomic operations for thread-safe access
read(index: number): number {
return Atomics.load(this.sharedArray, index);
}
write(index: number, value: number): void {
Atomics.store(this.sharedArray, index, value);
}
increment(index: number): number {
return Atomics.add(this.sharedArray, index, 1);
}
compareExchange(index: number, expectedValue: number, newValue: number): number {
return Atomics.compareExchange(this.sharedArray, index, expectedValue, newValue);
}
// Wait for value change (blocking)
waitFor(index: number, value: number, timeout?: number): 'ok' | 'timed-out' {
return Atomics.wait(this.sharedArray, index, value, timeout);
}
// Notify waiting threads
notify(index: number, count: number = 1): number {
return Atomics.notify(this.sharedArray, index, count);
}
terminate(): void {
this.worker.terminate();
}
}
// Worker side implementation
// worker/sharedMemory.worker.ts
const ctx: Worker = self as any;
let sharedArray: Int32Array | null = null;
ctx.addEventListener('message', (event) => {
if (event.data.type === 'INIT_SHARED_MEMORY') {
sharedArray = new Int32Array(event.data.buffer);
// Start processing with shared memory
processSharedData();
}
});
function processSharedData() {
if (!sharedArray) return;
// Example: Increment counter in shared memory
setInterval(() => {
// Atomic increment at index 0
const newValue = Atomics.add(sharedArray, 0, 1);
// Notify main thread of update
Atomics.notify(sharedArray, 0, 1);
}, 100);
}Real-World Use Cases
Case 1: Data Visualization Processing
// components/DataVisualizer.tsx
interface DataPoint {
x: number;
y: number;
value: number;
}
function DataVisualizer({ rawData }: { rawData: number[][] }) {
const [processedData, setProcessedData] = useState<DataPoint[]>([]);
const [clusters, setClusters] = useState<DataPoint[][]>([]);
const { postMessage, isProcessing, progress } = useWebWorker('../worker/dataProcessor.worker.ts');
useEffect(() => {
processData();
}, [rawData]);
const processData = async () => {
try {
// Process raw data in worker
const processed = await postMessage<DataPoint[]>({
type: 'PROCESS_DATA',
data: rawData,
});
setProcessedData(processed);
// Perform clustering in worker
const clusterResult = await postMessage<DataPoint[][]>({
type: 'CLUSTER_DATA',
data: processed,
config: { k: 5, iterations: 100 },
});
setClusters(clusterResult);
} catch (error) {
console.error('Data processing failed:', error);
}
};
return (
<div className="data-visualizer">
{isProcessing ? (
<div className="loading">Processing data... {progress.toFixed(0)}%</div>
) : (
<Canvas data={processedData} clusters={clusters} width={800} height={600} />
)}
</div>
);
}Case 2: Real-Time Search with Worker
// components/WorkerSearch.tsx
function WorkerSearch({ dataset }: { dataset: Item[] }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Item[]>([]);
const [searchTime, setSearchTime] = useState(0);
const searchWorkerRef = useRef<Worker | null>(null);
useEffect(() => {
// Initialize search worker with dataset
searchWorkerRef.current = new Worker(new URL('../worker/search.worker.ts', import.meta.url), {
type: 'module',
});
const worker = searchWorkerRef.current;
// Index the dataset in the worker
worker.postMessage({
type: 'INDEX_DATA',
data: dataset,
});
worker.onmessage = (event) => {
if (event.data.type === 'SEARCH_RESULTS') {
setResults(event.data.results);
setSearchTime(event.data.time);
}
};
return () => {
worker.terminate();
};
}, [dataset]);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
// Debounce search
const timer = setTimeout(() => {
searchWorkerRef.current?.postMessage({
type: 'SEARCH',
query,
});
}, 300);
return () => clearTimeout(timer);
}, [query]);
return (
<div className="worker-search">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search dataset..."
/>
{searchTime > 0 && (
<div className="search-stats">
Found {results.length} results in {searchTime.toFixed(2)}ms
</div>
)}
<div className="results">
{results.map((item) => (
<SearchResult key={item.id} item={item} />
))}
</div>
</div>
);
}Case 3: Background Sync and Caching
// utils/syncWorker.ts
export class SyncWorker {
private worker: Worker;
private syncQueue: Set<string> = new Set();
constructor() {
this.worker = new Worker(new URL('../worker/sync.worker.ts', import.meta.url), {
type: 'module',
});
this.setupMessageHandler();
this.setupPeriodicSync();
}
private setupMessageHandler() {
this.worker.onmessage = (event) => {
const { type, id, success } = event.data;
if (type === 'SYNC_COMPLETE') {
this.syncQueue.delete(id);
// Notify React components
window.dispatchEvent(
new CustomEvent('sync-complete', {
detail: { id, success },
}),
);
}
};
}
private setupPeriodicSync() {
// Sync every 30 seconds
setInterval(() => {
this.syncPendingData();
}, 30000);
// Sync when online
window.addEventListener('online', () => {
this.syncPendingData();
});
}
async queueForSync(id: string, data: any) {
this.syncQueue.add(id);
// Store in IndexedDB for persistence
await this.storeLocal(id, data);
// Try immediate sync if online
if (navigator.onLine) {
this.worker.postMessage({
type: 'SYNC_ITEM',
id,
data,
});
}
}
private async storeLocal(id: string, data: any) {
// Store in IndexedDB (simplified)
const db = await this.openDB();
const tx = db.transaction(['pending'], 'readwrite');
await tx.objectStore('pending').put({ id, data, timestamp: Date.now() });
}
private async syncPendingData() {
if (!navigator.onLine) return;
const pending = await this.getPendingItems();
pending.forEach((item) => {
this.worker.postMessage({
type: 'SYNC_ITEM',
id: item.id,
data: item.data,
});
});
}
private async openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open('SyncDB', 1);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains('pending')) {
db.createObjectStore('pending', { keyPath: 'id' });
}
};
});
}
private async getPendingItems(): Promise<any[]> {
const db = await this.openDB();
const tx = db.transaction(['pending'], 'readonly');
const store = tx.objectStore('pending');
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
// Hook for background sync
export function useBackgroundSync() {
const syncWorkerRef = useRef<SyncWorker | null>(null);
const [syncStatus, setSyncStatus] = useState<Map<string, boolean>>(new Map());
useEffect(() => {
syncWorkerRef.current = new SyncWorker();
const handleSyncComplete = (event: CustomEvent) => {
const { id, success } = event.detail;
setSyncStatus((prev) => new Map(prev).set(id, success));
};
window.addEventListener('sync-complete', handleSyncComplete as any);
return () => {
window.removeEventListener('sync-complete', handleSyncComplete as any);
};
}, []);
const queueSync = useCallback(async (id: string, data: any) => {
await syncWorkerRef.current?.queueForSync(id, data);
setSyncStatus((prev) => new Map(prev).set(id, false)); // Pending
}, []);
return {
queueSync,
syncStatus,
isSynced: (id: string) => syncStatus.get(id) === true,
isPending: (id: string) => syncStatus.get(id) === false,
};
}Performance Monitoring and Debugging
// utils/workerPerformance.ts
export class WorkerPerformanceMonitor {
private metrics: Map<string, number[]> = new Map();
private startTimes: Map<string, number> = new Map();
startMeasure(id: string) {
this.startTimes.set(id, performance.now());
}
endMeasure(id: string): number {
const startTime = this.startTimes.get(id);
if (!startTime) return 0;
const duration = performance.now() - startTime;
// Store metric
if (!this.metrics.has(id)) {
this.metrics.set(id, []);
}
this.metrics.get(id)!.push(duration);
this.startTimes.delete(id);
return duration;
}
getStats(id: string) {
const times = this.metrics.get(id) || [];
if (times.length === 0) return null;
const sorted = [...times].sort((a, b) => a - b);
return {
count: times.length,
mean: times.reduce((a, b) => a + b, 0) / times.length,
median: sorted[Math.floor(sorted.length / 2)],
min: sorted[0],
max: sorted[sorted.length - 1],
p95: sorted[Math.floor(sorted.length * 0.95)],
p99: sorted[Math.floor(sorted.length * 0.99)],
};
}
report() {
console.group('Worker Performance Report');
this.metrics.forEach((times, id) => {
const stats = this.getStats(id);
if (stats) {
console.table({
Operation: id,
'Avg Time': `${stats.mean.toFixed(2)}ms`,
P95: `${stats.p95.toFixed(2)}ms`,
Count: stats.count,
});
}
});
console.groupEnd();
}
}
// Debug component for worker monitoring
function WorkerDebugPanel() {
const [workerStats, setWorkerStats] = useState<any[]>([]);
useEffect(() => {
const interval = setInterval(() => {
// Get all active workers (custom tracking needed)
const stats = getActiveWorkerStats();
setWorkerStats(stats);
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<div className="worker-debug-panel">
<h3>Active Workers</h3>
<table>
<thead>
<tr>
<th>Worker</th>
<th>Status</th>
<th>Messages</th>
<th>Avg Time</th>
</tr>
</thead>
<tbody>
{workerStats.map((stat) => (
<tr key={stat.id}>
<td>{stat.name}</td>
<td>{stat.status}</td>
<td>{stat.messageCount}</td>
<td>{stat.avgTime.toFixed(2)}ms</td>
</tr>
))}
</tbody>
</table>
</div>
);
}Best Practices and Pitfalls
// Common patterns and anti-patterns
// ✅ Good: Reuse workers for multiple operations
const worker = new Worker('./worker.js');
worker.postMessage({ type: 'TASK_1', data });
// Later...
worker.postMessage({ type: 'TASK_2', data });
// ❌ Bad: Create new worker for each operation
const worker1 = new Worker('./worker.js');
worker1.postMessage(data1);
worker1.terminate();
const worker2 = new Worker('./worker.js');
worker2.postMessage(data2);
worker2.terminate();
// ✅ Good: Transfer large data efficiently
const buffer = new ArrayBuffer(1024 * 1024);
worker.postMessage({ buffer }, [buffer]); // Transfer ownership
// ❌ Bad: Clone large data unnecessarily
const largeArray = new Float32Array(1000000);
worker.postMessage({ array: largeArray }); // Clones entire array
// ✅ Good: Handle worker errors gracefully
worker.onerror = (error) => {
console.error('Worker error:', error);
// Fallback to main thread processing
processOnMainThread(data);
};
// ❌ Bad: No error handling
worker.postMessage(data); // Hope for the best
// ✅ Good: Clean up workers properly
useEffect(() => {
const worker = new Worker('./worker.js');
return () => {
worker.terminate();
};
}, []);
// ❌ Bad: Memory leak from unterminated workers
useEffect(() => {
const worker = new Worker('./worker.js');
// No cleanup!
}, []);Wrapping Up
Web Workers are your escape hatch from JavaScript’s single-threaded limitations. They let you run expensive computations without freezing your UI, process data in parallel on multi-core devices, and create truly responsive React applications even under heavy load.
The key is knowing when to use them (computations over 16ms, large data processing, background sync) and when not to (simple calculations, frequent small operations, DOM manipulation). With proper TypeScript typing, efficient data transfer using Transferable objects, and careful lifecycle management, Workers become a powerful tool in your React performance arsenal.