Plugin Workers
How to offload heavy computation (AI inference, encoding, complex algorithms) to dedicated Web Workers within your plugin.
Design Principle
The core engine has a shared Worker for pixel operations (merge, transform, transcode). Plugins must never invade the core Worker. Instead, create your own Worker within your plugin directory.
Benefits:
- On-demand loading — Users who don't use your feature pay no startup cost
- Dependency isolation — WASM modules, AI model libraries stay in your Worker chunk
- Fault isolation — Your Worker crash doesn't affect core merge/transform
- Bundler-friendly —
new URL('./worker.ts', import.meta.url)triggers automatic code-splitting
Directory Structure
MyPlugin/
├── index.tsx
├── protocols.ts
├── commands.ts
├── hooks.ts
├── components.tsx
└── worker/ # Plugin-private Worker directory
├── protocol.ts # Message types (Request/Response/Progress)
├── client.ts # Main-thread client (lazy init, request dispatch)
└── my-task.worker.ts # Worker implementation
Two Lifecycle Modes
Mode A: Fire-and-Forget (Lightweight)
For simple, infrequent, stateless tasks:
// In commands.ts
execute: async (ctx) => {
ctx.scoped!.setBusy(true);
try {
const worker = new Worker(
new URL('./worker/encode.worker.ts', import.meta.url),
{ type: 'module' }
);
const result = await new Promise((resolve, reject) => {
worker.onmessage = (e) => { resolve(e.data); worker.terminate(); };
worker.onerror = (e) => { reject(e); worker.terminate(); };
worker.postMessage(payload, [transferable]);
});
// Use result...
} finally {
ctx.scoped!.setBusy(false);
}
}
Mode B: Persistent Singleton (High-Frequency)
For tasks with expensive initialization (AI model loading, WASM warm-up):
// worker/protocol.ts
export interface WorkerRequest {
type: 'run';
reqId: number;
payload: ImageData;
}
export interface ProgressMessage {
type: 'progress';
reqId: number;
stage: 'downloading' | 'processing' | 'postprocessing';
progress: number; // 0.0 ~ 1.0
}
export interface ResultMessage {
type: 'result';
reqId: number;
data: Uint8Array;
}
export interface ErrorMessage {
type: 'error';
reqId: number;
error: string;
}
export type WorkerMessage = ProgressMessage | ResultMessage | ErrorMessage;
// worker/client.ts
let worker: Worker | null = null;
let reqCounter = 0;
function ensure(): Worker {
if (!worker) {
worker = new Worker(
new URL('./my-task.worker.ts', import.meta.url),
{ type: 'module' }
);
}
return worker;
}
export function run(payload: ImageData): Promise<Uint8Array> {
const w = ensure();
const reqId = ++reqCounter;
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Worker timeout'));
dispose();
}, 30000);
w.onmessage = (e: MessageEvent<WorkerMessage>) => {
if (e.data.reqId !== reqId) return; // stale response guard
if (e.data.type === 'result') {
clearTimeout(timeout);
resolve(e.data.data);
} else if (e.data.type === 'error') {
clearTimeout(timeout);
reject(new Error(e.data.error));
}
// 'progress' messages can drive UI updates
};
w.postMessage({ type: 'run', reqId, payload }, [payload.data.buffer]);
});
}
export function dispose() {
worker?.terminate();
worker = null;
}
Mandatory Rules
| # | Rule | Reason |
|---|---|---|
| 1 | Use Transferable for large buffers | postMessage(msg, [buffer]) — zero-copy transfer. Never structured-clone megabyte ArrayBuffers |
| 2 | Timeout protection | Tasks > 3s MUST have a timeout. On timeout: terminate() + show error to user |
| 3 | No main-thread blocking | Never use Atomics.wait or synchronous waits for Worker responses |
| 4 | Error bubbling | Worker errors → postMessage error back → main thread shows toast/HUD |
| 5 | No DOM in Workers | Workers cannot import React, document, window — will crash on startup |
| 6 | setBusy(true/false) | Always wrap async Worker calls in setBusy with a finally block |
Progress Reporting
For long-running tasks (AI inference, model downloads), report progress:
// Inside the worker
self.postMessage({
type: 'progress',
reqId,
stage: 'downloading',
progress: 0.45,
detail: 'Loading model weights...',
});
The main-thread client can forward progress to UI via signals or direct ref updates.
Next Steps
- Plugin API Reference —
ctx.scoped.setBusyand related APIs - Your First Plugin — Basic plugin structure
- Security & Isolation — Fault isolation guarantees
Last updated: 2026-07-30