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

39 stars

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

$curl -o ~/.claude/skills/web-workers/SKILL.md --create-dirs "https://raw.githubusercontent.com/InugamiDev/ultrathink-oss/main/.claude/skills/web-workers/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/web-workers/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How web-workers Compares

Feature / Agentweb-workersStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/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

39
from InugamiDev/ultrathink-oss

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

39
from InugamiDev/ultrathink-oss

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

39
from InugamiDev/ultrathink-oss

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

39
from InugamiDev/ultrathink-oss

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

39
from InugamiDev/ultrathink-oss

> TypeScript-first schema validation with static type inference.

webinar-registration-page

39
from InugamiDev/ultrathink-oss

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

39
from InugamiDev/ultrathink-oss

Webhook design patterns — delivery, retry with exponential backoff, HMAC signature verification, payload validation, idempotency keys

web-vitals

39
from InugamiDev/ultrathink-oss

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

39
from InugamiDev/ultrathink-oss

Native Web Components, custom elements API, Shadow DOM, HTML templates, slots, lifecycle callbacks, and framework-agnostic design patterns

wasm

39
from InugamiDev/ultrathink-oss

WebAssembly integration — Rust to WASM with wasm-pack/wasm-bindgen, WASI, browser usage, server-side WASM, and performance considerations

vue

39
from InugamiDev/ultrathink-oss

Vue 3 Composition API, Nuxt patterns, reactivity system, component architecture, and production development practices

Vitest

39
from InugamiDev/ultrathink-oss

> Blazing fast unit testing powered by Vite — Jest-compatible API, native ESM, TypeScript.