web-workers
Offload heavy computation from the main thread using Web Workers, SharedWorkers, and Comlink — structured messaging, transferable objects, and off-main-thread architecture patterns
Best use case
web-workers is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Offload heavy computation from the main thread using Web Workers, SharedWorkers, and Comlink — structured messaging, transferable objects, and off-main-thread architecture patterns
Teams using web-workers should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/web-workers/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How web-workers Compares
| Feature / Agent | web-workers | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Offload heavy computation from the main thread using Web Workers, SharedWorkers, and Comlink — structured messaging, transferable objects, and off-main-thread architecture patterns
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Web Workers Skill
## Purpose
The browser's main thread handles DOM rendering, event listeners, and JavaScript execution on a single thread. Any computation taking more than ~50ms blocks user interaction and causes visible jank. Web Workers run JavaScript on separate OS threads, keeping the UI responsive while performing heavy computation — parsing, sorting, image processing, cryptography, data transformation, or WASM execution.
## Key Concepts
### Worker Types
| Type | Scope | Use Case |
|------|-------|----------|
| **Dedicated Worker** | Single page | Heavy computation for one tab |
| **SharedWorker** | Multiple tabs/frames on same origin | Shared WebSocket, cross-tab sync |
| **Service Worker** | Entire origin (network proxy) | Offline support, push notifications |
### Communication Model
```
Main Thread ←→ Worker Thread
postMessage(data) → onmessage(event)
onmessage(event) ← postMessage(data)
Data is COPIED by default (structured clone algorithm).
Transferable objects can be MOVED (zero-copy) for ArrayBuffers.
SharedArrayBuffer allows TRUE shared memory (requires COOP/COEP headers).
```
### What Workers Cannot Access
- DOM (`document`, `window.document`)
- `window` (workers get `self` / `globalThis`)
- `localStorage` / `sessionStorage`
- Synchronous XHR (only in workers, but avoid it)
### What Workers Can Access
- `fetch`, `WebSocket`, `IndexedDB`, `Cache API`
- `crypto.subtle`, `TextEncoder/Decoder`
- `importScripts()` (classic) or ES modules
- `setTimeout`, `setInterval`, `requestAnimationFrame` (not in all workers)
- WASM instantiation and execution
## Implementation
### Basic Dedicated Worker
```typescript
// worker.ts
self.onmessage = (event: MessageEvent<{ numbers: number[] }>) => {
const { numbers } = event.data;
// Heavy computation: sort + statistical analysis
const sorted = [...numbers].sort((a, b) => a - b);
const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;
const median = sorted[Math.floor(sorted.length / 2)];
self.postMessage({ sorted, mean, median });
};
```
```typescript
// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url), {
type: 'module',
});
worker.onmessage = (event) => {
const { sorted, mean, median } = event.data;
console.log('Results:', { mean, median });
};
worker.onerror = (error) => {
console.error('Worker error:', error.message);
};
worker.postMessage({ numbers: generateLargeDataset() });
```
### Typed Worker Wrapper
```typescript
// worker-types.ts
export interface WorkerRequest {
id: string;
type: 'sort' | 'filter' | 'transform';
payload: unknown;
}
export interface WorkerResponse {
id: string;
result?: unknown;
error?: string;
}
// typed-worker.ts
type Resolver = {
resolve: (value: unknown) => void;
reject: (reason: Error) => void;
};
export function createTypedWorker(url: URL) {
const worker = new Worker(url, { type: 'module' });
const pending = new Map<string, Resolver>();
let counter = 0;
worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
const { id, result, error } = event.data;
const resolver = pending.get(id);
if (!resolver) return;
pending.delete(id);
if (error) {
resolver.reject(new Error(error));
} else {
resolver.resolve(result);
}
};
return {
send<T>(type: string, payload: unknown): Promise<T> {
const id = `msg-${++counter}`;
return new Promise((resolve, reject) => {
pending.set(id, { resolve: resolve as (v: unknown) => void, reject });
worker.postMessage({ id, type, payload });
});
},
terminate() {
worker.terminate();
for (const [, { reject }] of pending) {
reject(new Error('Worker terminated'));
}
pending.clear();
},
};
}
// Usage
const w = createTypedWorker(new URL('./worker.ts', import.meta.url));
const result = await w.send<number[]>('sort', { data: largeArray });
```
### Comlink (RPC-Style Workers)
```typescript
// heavy-math.worker.ts
import * as Comlink from 'comlink';
const api = {
async fibonacci(n: number): Promise<number> {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
},
async processImage(
imageData: ImageData,
filter: 'grayscale' | 'blur' | 'sharpen'
): Promise<ImageData> {
const { data, width, height } = imageData;
const output = new Uint8ClampedArray(data.length);
if (filter === 'grayscale') {
for (let i = 0; i < data.length; i += 4) {
const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
output[i] = output[i + 1] = output[i + 2] = gray;
output[i + 3] = data[i + 3];
}
}
return new ImageData(output, width, height);
},
};
export type HeavyMathAPI = typeof api;
Comlink.expose(api);
```
```typescript
// main.ts — call worker like a normal async function
import * as Comlink from 'comlink';
import type { HeavyMathAPI } from './heavy-math.worker';
const worker = new Worker(
new URL('./heavy-math.worker.ts', import.meta.url),
{ type: 'module' }
);
const math = Comlink.wrap<HeavyMathAPI>(worker);
// Looks like a regular function call — Comlink handles serialization
const result = await math.fibonacci(50);
console.log('Fibonacci(50):', result);
```
### React Hook for Workers
```typescript
// useWorker.ts
import { useEffect, useRef, useCallback, useState } from 'react';
interface UseWorkerOptions {
terminateOnUnmount?: boolean;
}
export function useWorker<TInput, TOutput>(
workerFactory: () => Worker,
options: UseWorkerOptions = { terminateOnUnmount: true }
) {
const workerRef = useRef<Worker | null>(null);
const [result, setResult] = useState<TOutput | null>(null);
const [error, setError] = useState<Error | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
useEffect(() => {
const worker = workerFactory();
workerRef.current = worker;
worker.onmessage = (event: MessageEvent<TOutput>) => {
setResult(event.data);
setIsProcessing(false);
};
worker.onerror = (err) => {
setError(new Error(err.message));
setIsProcessing(false);
};
return () => {
if (options.terminateOnUnmount) {
worker.terminate();
}
};
}, []);
const postMessage = useCallback((data: TInput) => {
setIsProcessing(true);
setError(null);
workerRef.current?.postMessage(data);
}, []);
return { result, error, isProcessing, postMessage };
}
// Usage in component
function DataProcessor({ data }: { data: number[] }) {
const { result, isProcessing, postMessage } = useWorker<number[], number[]>(
() => new Worker(new URL('./sort.worker.ts', import.meta.url), { type: 'module' })
);
return (
<button onClick={() => postMessage(data)} disabled={isProcessing}>
{isProcessing ? 'Processing...' : 'Sort Data'}
</button>
);
}
```
### Transferable Objects (Zero-Copy)
```typescript
// Transfer ownership of ArrayBuffer instead of copying
const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100MB
const uint8 = new Uint8Array(buffer);
// ... fill with data ...
// COPY (slow for large buffers):
// worker.postMessage({ data: uint8 });
// TRANSFER (instant, zero-copy — buffer becomes unusable in sender):
worker.postMessage({ data: buffer }, [buffer]);
// buffer.byteLength is now 0 — ownership transferred
// Worker side: receive transferred buffer
self.onmessage = (event) => {
const buffer: ArrayBuffer = event.data.data;
const view = new Uint8Array(buffer);
// ... process ...
// Transfer back to main thread
self.postMessage({ result: buffer }, [buffer]);
};
```
### SharedWorker (Cross-Tab Communication)
```typescript
// shared-ws.worker.ts
const connections: MessagePort[] = [];
let socket: WebSocket | null = null;
function broadcast(message: unknown) {
for (const port of connections) {
port.postMessage(message);
}
}
self.onconnect = (event: MessageEvent) => {
const port = event.ports[0];
connections.push(port);
// Initialize WebSocket once, shared across all tabs
if (!socket) {
socket = new WebSocket('wss://api.example.com/ws');
socket.onmessage = (wsEvent) => {
broadcast({ type: 'ws-message', data: JSON.parse(wsEvent.data) });
};
}
port.onmessage = (msgEvent) => {
if (msgEvent.data.type === 'send') {
socket?.send(JSON.stringify(msgEvent.data.payload));
}
};
port.start();
};
```
```typescript
// main.ts
const shared = new SharedWorker(
new URL('./shared-ws.worker.ts', import.meta.url),
{ type: 'module', name: 'ws-shared' }
);
shared.port.onmessage = (event) => {
console.log('Message from shared worker:', event.data);
};
shared.port.start();
shared.port.postMessage({ type: 'send', payload: { action: 'subscribe' } });
```
### Vite Worker Configuration
```typescript
// vite.config.ts
export default defineConfig({
worker: {
format: 'es', // Use ES modules in workers
plugins: () => [], // Plugins applied inside workers
rollupOptions: {
output: {
entryFileNames: 'assets/worker-[name]-[hash].js',
},
},
},
});
// Workers are auto-detected with `new Worker(new URL(...), { type: 'module' })`
// or with the `?worker` suffix:
import MyWorker from './my-worker?worker';
const worker = new MyWorker();
```
## Best Practices
1. **Measure before offloading.** Use the Performance API or Chrome DevTools to confirm a task actually blocks the main thread for >50ms. Workers add message-passing overhead.
2. **Batch messages.** Instead of sending one message per item, batch data into chunks. The structured clone overhead is per-message, not per-byte.
3. **Use Transferable objects** for large ArrayBuffers. Structured cloning a 100MB buffer takes ~80ms; transferring it takes ~0ms.
4. **Pool workers for repeated tasks.** Creating a new Worker has startup cost (~50-100ms). Reuse workers and route tasks via message types.
5. **Always handle errors.** Workers fail silently without `onerror`. Add error handlers and consider a timeout mechanism for unresponsive workers.
6. **Use Comlink for complex APIs.** Once a worker has more than 2-3 message types, Comlink's RPC pattern drastically reduces boilerplate.
## Common Pitfalls
| Pitfall | Symptom | Fix |
|---------|---------|-----|
| Copying large data via `postMessage` | High memory usage, slow transfer | Use `Transferable` objects or `SharedArrayBuffer` |
| Not terminating workers | Memory leaks, zombie threads | Call `worker.terminate()` on component unmount or page unload |
| Accessing DOM from worker | `ReferenceError: document is not defined` | Workers have no DOM access — send results back to main thread for DOM updates |
| Module workers not supported in older browsers | Worker fails to load | Add `type: 'module'` and check `Worker` constructor support; fall back to classic workers |
| SharedArrayBuffer without COOP/COEP headers | `SharedArrayBuffer is not defined` | Set `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp` |
| Race conditions in SharedWorker | Inconsistent state across tabs | Use structured message protocol with sequence IDs; avoid shared mutable state |
| Worker bundle too large | Slow worker startup | Code-split worker dependencies; keep worker bundles focused |
| Forgetting `port.start()` on SharedWorker | Messages never received | Always call `port.start()` after setting up `onmessage` |Related Skills
ultrathink
UltraThink Workflow OS — 4-layer skill mesh with persistent memory and privacy hooks for complex engineering tasks. Routes prompts through intent detection to activate the right domain skills automatically.
ultrathink_review
Multi-pass code review powered by UltraThink's quality gate — checks correctness, security (OWASP), performance, readability, and project conventions in a single structured pass.
ultrathink_memory
Persistent memory system for UltraThink — search, save, and recall project context, decisions, and patterns across sessions using Postgres-backed fuzzy search with synonym expansion.
ui-design
Comprehensive UI design system: 230+ font pairings, 48 themes, 65 design systems, 23 design languages, 30 UX laws, 14 color systems, Swiss grid, Gestalt principles, Pencil.dev workflow. Inherits ui-ux-pro-max (99 UX rules) + impeccable-frontend-design (anti-AI-slop). Triggers on any design, UI, layout, typography, color, theme, or styling task.
Zod
> TypeScript-first schema validation with static type inference.
webinar-registration-page
Build a webinar or live event registration page as a self-contained HTML file with countdown timer, speaker bio, agenda, and registration form. Triggers on: "build a webinar registration page", "create a webinar sign-up page", "event registration landing page", "live training registration page", "workshop sign-up page", "create a webinar page", "build an event page", "free webinar landing page", "live demo registration page", "online event page", "create a registration page for my webinar", "build a training event page".
webhooks
Webhook design patterns — delivery, retry with exponential backoff, HMAC signature verification, payload validation, idempotency keys
web-vitals
Core Web Vitals monitoring (LCP, FID, CLS, INP, TTFB), measurement with web-vitals library, reporting to analytics, and optimization strategies for Next.js
web-components
Native Web Components, custom elements API, Shadow DOM, HTML templates, slots, lifecycle callbacks, and framework-agnostic design patterns
wasm
WebAssembly integration — Rust to WASM with wasm-pack/wasm-bindgen, WASI, browser usage, server-side WASM, and performance considerations
vue
Vue 3 Composition API, Nuxt patterns, reactivity system, component architecture, and production development practices
Vitest
> Blazing fast unit testing powered by Vite — Jest-compatible API, native ESM, TypeScript.