Web Workers: Running CPU-Intensive Code Without Freezing the UI
JavaScript is single-threaded. Heavy computation on the main thread blocks the UI — no animations, no input response, nothing until the work is done.
Web Workers run JavaScript in a background thread, keeping the UI responsive while heavy work happens in parallel.
When to Use Workers
Use a Web Worker when a task takes more than ~16ms (one frame) and involves:
- Parsing large JSON or CSV files
- Image processing or canvas manipulation
- Cryptographic operations
- Complex data transformations or sorting
- Any tight loop over large datasets
Basic Worker Setup
// worker.ts — runs in a separate thread
self.addEventListener('message', (event: MessageEvent) => {
const { data, operation } = event.data;
if (operation === 'sort') {
// This can take 500ms without freezing the UI
const sorted = data.sort((a: number, b: number) => a - b);
self.postMessage({ result: sorted });
}
});
// main.ts — UI thread
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
worker.addEventListener('message', (event) => {
const { result } = event.data;
setData(result); // Update UI with sorted data
});
function sortInBackground(data: number[]) {
worker.postMessage({ operation: 'sort', data });
// Returns immediately — UI stays responsive
}
Transferable Objects (Zero-Copy)
By default, data is copied when sent to/from a worker (structured clone). For large ArrayBuffers, use transferable objects to move ownership instead:
// Transfer ownership of buffer — no copy, instant
const buffer = new ArrayBuffer(100 * 1024 * 1024); // 100MB
worker.postMessage({ buffer }, [buffer]); // buffer is now owned by worker
// buffer is no longer accessible here (transferred)
Comlink: The Clean API
Raw postMessage is verbose. Comlink wraps workers in a proxy that looks like a regular async function call:
// worker.ts
import { expose } from 'comlink';
const api = {
async processImage(imageData: ImageData): Promise<ImageData> {
// Heavy image processing
return applyFilter(imageData);
},
async parseCSV(csv: string): Promise<Record<string, string>[]> {
return parseAndTransform(csv);
},
};
expose(api);
// main.ts
import { wrap } from 'comlink';
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
const api = wrap<typeof api>(worker);
// Looks like a normal async call — runs in worker thread
const result = await api.parseCSV(csvString);
Worker Pool for Parallel Tasks
For multiple independent tasks, a worker pool saturates all CPU cores:
class WorkerPool {
private workers: Worker[] = [];
private queue: Array<{ resolve: Function; reject: Function; task: any }> = [];
private idle: Worker[] = [];
constructor(size = navigator.hardwareConcurrency) {
for (let i = 0; i < size; i++) {
const w = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
this.workers.push(w);
this.idle.push(w);
}
}
run(task: any): Promise<any> {
return new Promise((resolve, reject) => {
const worker = this.idle.pop();
if (worker) {
this.execute(worker, task, resolve, reject);
} else {
this.queue.push({ resolve, reject, task });
}
});
}
}
The Ship Fast Pattern
Web Workers, optimistic updates, code splitting, and the other performance patterns that separate fast apps from slow ones are part of the Ship Fast Skill Pack — a set of Claude Code skills that implement these patterns as reusable slash commands.
Top comments (0)