Advanced Features
Transferables
Into the worker, via RunOptions.transfer:
const buffer = new ArrayBuffer(1024 * 1024)
const result = await run(buffer, { transfer: [buffer] })
// buffer.byteLength === 0 immediately — it was detached, not copiedBack out of the worker, via ctx.transfer(...) — the mirror of the above, for a handler that wants to hand back a large buffer (e.g. a resized image, an OffscreenCanvas-rendered frame) without copying it:
// resize.worker.ts
export default defineWorkerHandler((input: ResizeInput, ctx) => {
const output = resize(input) // produces a fresh ArrayBuffer
ctx.transfer(output) // sent back zero-copy instead of structured-clone copied
return output
})ctx.transfer() doesn't require the transferred object to be part of the returned value — call it with whatever transferables should ride along with the result. Safe to call more than once; every object passed across all calls is included.
Streaming / Chunked Results
For large datasets where you want intermediate results without waiting for full completion:
import { useWorker } from 'vue-worker-kit'
const { run, chunks, isRunning } = useWorker<typeof import('./process.worker')>(
() => new Worker(new URL('./process.worker.ts', import.meta.url), { type: 'module' }),
{ streaming: true }, // required — without it `chunks` is `undefined`, not a ref
)
// Process large dataset with streaming results
const finalResult = await run(largeDataset)
// chunks.value contains all intermediate results as they arrive
watch(chunks, (newChunks) => {
console.log('Received chunk:', newChunks[newChunks.length - 1])
})Worker-side:
// process.worker.ts
import { defineWorkerHandler } from 'vue-worker-kit/worker'
export default defineWorkerHandler(async (items: LargeDataset[], ctx) => {
const results: Result[] = []
for (let i = 0; i < items.length; i += 100) {
const batch = items.slice(i, i + 100)
const processed = await processBatch(batch)
// Send intermediate result immediately
ctx.reportChunk(processed)
results.push(...processed)
if (ctx.signal.aborted) throw ctx.signal.reason
}
return results // Final result
})chunks: ShallowRef<unknown[]>— reactive array of all reported chunksctx.reportChunk(data)— sends partial result to main thread (unthrottled)- Chunks accumulate in order; final result is separate from chunks
- Useful for progressive rendering, real-time updates, or memory-efficient processing
Cancellation
const controller = new AbortController()
const promise = run(input, { signal: controller.signal })
controller.abort() // promise rejects with AbortError, immediately — regardless of what the worker doesIf you don't pass your own signal, run() creates one internally; cancel() aborts it. retries never applies to an aborted run.
Retry Strategy with Backoff
For transient failures, configure automatic retries with exponential backoff:
import { useWorker } from 'vue-worker-kit'
const { run, error } = useWorker<typeof import('./api.worker')>(
() => new Worker(new URL('./api.worker.ts', import.meta.url), { type: 'module' }),
{
retries: 3,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 10000), // 1s, 2s, 4s, capped at 10s
},
)
// On failure, automatically retries with increasing delay
const result = await run(data)retries: number— max retry attempts (default:0, no retries)retryDelay: number | ((attempt: number) => number)— delay between retries- If
number: constant delay in ms - If function: dynamic delay based on attempt number (1-indexed)
- If
- Retries only apply to non-abort errors (
AbortErrorrejects immediately) - Common pattern: exponential backoff with jitter for API calls or flaky operations
// Advanced: exponential backoff with random jitter
{
retries: 5,
retryDelay: (attempt) => {
const baseDelay = 1000 * 2 ** attempt
const jitter = Math.random() * 1000
return Math.min(baseDelay + jitter, 30000)
},
}Memoization / Result Cache
For pure worker functions (same input → same output), enable LRU caching:
import { useWorker } from 'vue-worker-kit'
const { run } = useWorker<typeof import('./hash.worker')>(
() => new Worker(new URL('./hash.worker.ts', import.meta.url), { type: 'module' }),
{
cache: { cache: 'lru', maxCacheSize: 100 }, // keep the last 100 results
},
)
// First call — executes in worker
const hash1 = await run(data)
// Second call with the same input (compared via JSON.stringify) — returns the cached
// result instantly, no worker invocation, no postMessage round-trip
const hash2 = await run(data) // hash1 === hash2Options (cache: UseWorkerCacheOptions):
cache: 'lru'— enable the LRU cache (unset/omitted disables it)maxCacheSize: number— max entries before evicting the oldest (default:50)
The cache key is JSON.stringify(input) (exported as createCacheKey() if you want to reason about collisions yourself) — inputs that stringify the same (including object key order) share a cache entry.
useWorkerComputed() doesn't have a cache option — its own generation-number mechanism already discards stale/superseded results, and its source() typically produces a fresh input on every reactive tick anyway, so key-based memoization wouldn't have much to hit.
Error handling
- A thrown error inside the handler is serialized as
{ name, message, stack }and reconstructed on the main thread as aWorkerError..workerStackis the original in-worker stack;.causeis a synthetic error created at therun()call site (before crossing into the worker) — so both ends of the failure show up together in the console/Sentry. - A protocol-level failure (e.g. an object that doesn't structured-clone) becomes a
WorkerErrorwithname: 'DataCloneError', not an unhandled exception. WorkerUnavailableErroris thrown instead of a rawReferenceError: Worker is not definedwhenrun()is called somewhere with no globalWorker(typically SSR) — it is never wrapped or retried.
Worker lifecycle
- Idle timeout — a worker idle longer than
idleTimeoutis terminated; the nextrun()transparently spins up a new one (small latency on the first call after idling — expected). - Scope-based auto-termination —
useWorker/useWorkerPoolcalled insidesetup()terminate their worker(s) ononScopeDispose, avoiding the classic SPA-navigation leak. - Pool workers are lazy — created as tasks arrive, up to
size, not all atcreateWorkerPool()time.
Warmup
To avoid cold-start latency on the first task, you can pre-create workers without executing any work:
// Single worker
const { warmup, run } = useWorker<typeof import('./x.worker')>(
() => new Worker(new URL('./x.worker.ts', import.meta.url), { type: 'module' }),
)
await warmup() // Worker is now instantiated and ready
const result = await run(data) // No worker creation delay
// Pool - pre-create all workers up to size
const pool = createWorkerPool<typeof import('./resize.worker')>(
() => new Worker(new URL('./resize.worker.ts', import.meta.url), { type: 'module' }),
{ size: 4 },
)
await pool.warmup() // All 4 workers are now instantiated
const results = await pool.map(items) // Immediate execution, no cold startsWarmup is useful when you know a worker-intensive operation is about to happen (e.g., user clicks "Process" button) and you want to eliminate the ~50-200ms worker creation latency. Call it during idle time (e.g., onMounted, or after initial page load) to keep interactions snappy.