Skip to content

Advanced Features ​

Transferables ​

Into the worker, via RunOptions.transfer:

ts
const buffer = new ArrayBuffer(1024 * 1024)
const result = await run(buffer, { transfer: [buffer] })
// buffer.byteLength === 0 immediately — it was detached, not copied

Back 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:

ts
// 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:

ts
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:

ts
// 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 chunks
  • ctx.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

If streaming is left false (the default) but the handler calls ctx.reportChunk() anyway, the chunk is dropped — there's no chunks ref for it to land in. In development mode you'll get a one-time console.warn per run pointing this out; in production it stays silent. If you're seeing that warning unexpectedly, either pass { streaming: true }, or, if this worker runs through createWorkerPool()/useWorkerPool(), pass onChunk to run()/map() instead — see Worker Pool.

Cancellation ​

ts
const controller = new AbortController()
const promise = run(input, { signal: controller.signal })
controller.abort() // promise rejects with AbortError, immediately — regardless of what the worker does

If 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:

ts
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)
  • Retries only apply to non-abort errors (AbortError rejects immediately)
  • Common pattern: exponential backoff with jitter for API calls or flaky operations
ts
// 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:

ts
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 === hash2

Options (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 a WorkerError. .workerStack is the original in-worker stack; .cause is a synthetic error created at the run() 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 WorkerError with name: 'DataCloneError', not an unhandled exception.
  • WorkerUnavailableError is thrown instead of a raw ReferenceError: Worker is not defined when run() is called somewhere with no global Worker (typically SSR) — it is never wrapped or retried.

Worker lifecycle ​

  • Idle timeout — a worker idle longer than idleTimeout is terminated; the next run() transparently spins up a new one (small latency on the first call after idling — expected).
  • Scope-based auto-termination — useWorker/useWorkerPool called inside setup() terminate their worker(s) on onScopeDispose, avoiding the classic SPA-navigation leak.
  • Pool workers are lazy — created as tasks arrive, up to size, not all at createWorkerPool() time.

Warmup ​

To avoid cold-start latency on the first task, you can pre-create workers without executing any work:

ts
// 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 starts

Warmup 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.

Benchmarks ​

npm run benchmark (benchmark/heavy-computation.bench.ts, via tinybench) compares a CPU-bound task (naive recursive fib(30..34)) run on the main thread vs. a single worker thread vs. a 4-thread pool — via node:worker_threads, since this runs under Node (tsx), not a browser. It's a sanity check of the pool's real parallel speedup, not a benchmark of this package's own composables (those add negligible overhead on top of raw postMessage, which is what's actually being measured here).

Example run on this machine (results vary by hardware/load — run it yourself for numbers that mean anything on your machine):

TaskOps/secAvg time
Main thread7.4135ms
Single worker thread5.8173ms
Pool of 4 worker threads9.1111ms

Single-worker is slower than main thread here — expected: postMessage/thread-startup overhead on a task that isn't parallelized. The pool is faster because 8 tasks genuinely run across 4 threads at once, not because any one worker is faster than the main thread.