js-spawn
v0.1.3
Published
Run functions in Web Workers with spawn(fn, ...args) and get a Promise back.
Maintainers
Readme
js-spawn
Run a function in a Web Worker with a single call.
js-spawn runs your function in a Worker thread so the main thread stays responsive.
setup (Vite) + first run
1) Install
pnpm add js-spawn
# or: npm i js-spawn
# or: yarn add js-spawn2) Add the Vite plugin
// vite.config.ts
import { defineConfig } from 'vite';
import { jsSpawnVitePlugin } from 'js-spawn/plugin';
export default defineConfig({
plugins: [jsSpawnVitePlugin()],
});3) Hello from Worker
// Main tread
import { spawn } from 'js-spawn';
await spawn(() => {
console.log('hello from worker');
});Bundler support
✅ Vite only (for now) Webpack/Rollup/esbuild support is planned for future releases.
Why
JavaScript runs your UI and your heavy work on the same thread. When heavy work happens on the main thread, everything freezes: typing, scrolling, animations.
js-spawn runs your function inside a Web Worker and gives you the result as a Promise.
What’s new (auto-capture)
Workers don’t share your module scope, so normally you must manually pass everything.
Now js-spawn automatically captures referenced variables and imports used inside your function and makes them available in the worker.
Captures variables
import { spawn } from 'js-spawn';
const salt = 123;
const out = await spawn(() => {
// `salt` is captured automatically
return 10 + salt;
});
console.log(out); // 133Captures modules (aliases included)
import axios from 'axios';
import { spawn } from 'js-spawn';
const url = 'https://example.com/data.json';
const data = await spawn(async () => {
const res = await axios.get(url); // axios + url are captured
return res.data;
});Usage examples
CPU-heavy work
import { spawn } from 'js-spawn';
const bytes = new Uint8Array([1, 2, 3, 4, 5, 6]);
const digest = await spawn(() => {
let h = 2166136261; // FNV-1a-ish
for (let i = 0; i < bytes.length; i++) {
h ^= bytes[i];
h = Math.imul(h, 16777619);
}
return (h >>> 0).toString(16);
});
console.log(digest);Async worker function
import { spawn } from 'js-spawn';
const result = await spawn(async () => {
await new Promise((r) => setTimeout(r, 50));
return 21 * 2;
});
console.log(result); // 42Errors reject the promise
import { spawn } from 'js-spawn';
try {
await spawn(() => {
throw new Error('boom');
});
} catch (e) {
console.error('Worker failed:', e);
}Mutation rules (important)
Most captured values are cloned into the worker (structured clone). That means mutations in the worker usually do not affect values on the main thread.
1) Plain objects/arrays: cloned (no shared mutation)
import { spawn } from 'js-spawn';
const state = { count: 0, items: ['a'] };
await spawn(() => {
// This is a clone inside the worker
state.count += 1;
state.items.push('b');
});
// Main thread is unchanged
console.log(state);
// => { count: 0, items: ["a"] }2) True shared mutation: SharedArrayBuffer
If you want both sides to see changes, use SharedArrayBuffer (often with Atomics).
import { spawn } from 'js-spawn';
const sab = new SharedArrayBuffer(4);
const a = new Int32Array(sab);
a[0] = 1;
await spawn(() => {
const w = new Int32Array(sab);
Atomics.add(w, 0, 41);
});
console.log(a[0]); // 423) Fast handoff (not shared): ArrayBuffer transfer
ArrayBuffer can be transferred for speed, but ownership moves.
If you need it back, return it.
import { spawn } from 'js-spawn';
let buf = new ArrayBuffer(4);
new Uint8Array(buf).set([1, 2, 3, 4]);
buf = await spawn(() => {
const v = new Uint8Array(buf);
v[0] = 99;
return buf; // send ownership back
});
console.log(new Uint8Array(buf)); // [99, 2, 3, 4]What you cannot send (based on runtime checks)
js-spawn validates captured values before running. These are rejected:
❌ Functions
import { spawn } from 'js-spawn';
const fn = () => 123;
await spawn(() => {
return fn; // ❌ functions cannot be sent to spawn
});❌ Symbols
import { spawn } from 'js-spawn';
const s = Symbol('x');
await spawn(() => s); // ❌ symbols cannot be sent❌ WeakMap / WeakSet
import { spawn } from 'js-spawn';
const wm = new WeakMap();
await spawn(() => wm); // ❌ WeakMap/WeakSet are not structured-cloneable❌ DOM nodes / Window
Workers don’t have DOM APIs, and DOM objects are not cloneable.
import { spawn } from 'js-spawn';
const el = document.querySelector('#app');
await spawn(() => el); // ❌ DOM nodes cannot be sent✅ Use plain data instead (strings, numbers, objects, typed arrays, etc.).
Structured clone rules (what is allowed)
Captured values and return values must be structured-cloneable.
✅ Commonly allowed:
- primitives (
string,number,boolean,null,undefined,bigint) - arrays and plain objects
DateArrayBuffer,SharedArrayBuffer, typed arraysMap,Set
❌ Commonly not allowed:
- functions, symbols
WeakMap,WeakSet- DOM nodes /
window - many class instances / proxies / React elements
If something fails,
js-spawnthrows an error pointing to the failing captured path.
Worker lifecycle & auto-destroy (debounced)
js-spawn automatically manages Worker instances for you.
To avoid keeping idle Workers alive forever, Workers are destroyed automatically after a short idle period using a debounced cleanup strategy.
How it works
- A Worker is reused while there are active or recent
spawn()calls. - When the Worker becomes idle, destruction is debounced.
- The Worker is terminated only if no tasks are in flight.
This avoids:
- killing a Worker in the middle of execution
- unnecessary Worker recreation during bursty workloads
You do not need to manually clean up or terminate Workers.
Important notes
- Destruction happens only when no tasks are pending
- Idle timeout is debounced, not immediate
- This behavior is internal and may be tuned in future versions
If your workload is bursty (many calls close together), this prevents unnecessary Worker churn. If your workload is infrequent, idle Workers will not stay alive forever.
Why this exists
Creating Workers is expensive. Keeping unused Workers alive is wasteful.
Debounced auto-destroy gives a balance between:
- performance
- memory usage
- correctness
SSR note
If you call spawn() in SSR (server runtime), it will throw because Worker doesn’t exist there.
Use it only on the client (after hydration), or disable SSR for the module/page that calls it.
License
MIT
