npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

js-spawn

v0.1.4

Published

Run a function in a Web Worker with spawn(fn) and get a Promise back.

Downloads

253

Readme

js-spawn

Move any function off the main thread by adding one word.

npm version npm downloads license types

const result = await spawn(() => heavyWork(data));

That runs in a Web Worker. Your UI never freezes.


The problem

Your app does something expensive — sorting 100k rows, parsing a big file, hashing, compressing an image. The browser has one thread for your JavaScript and your UI, so while that work runs, nothing else happens. Clicks queue up. Animations stutter. The tab looks broken.

Web Workers exist to fix this. But using one means a second file, a message protocol, and manually forwarding every variable your code needs:

// worker.ts — a whole separate file
self.onmessage = (e) => {
  const { rows, threshold } = e.data;
  self.postMessage(rows.filter((r) => r.score > threshold).length);
};
// main.ts — plumbing
const worker = new Worker(new URL('./worker.ts', import.meta.url), {
  type: 'module',
});

worker.postMessage({ rows, threshold });

const count = await new Promise((resolve) => {
  worker.onmessage = (e) => resolve(e.data);
});

worker.terminate();

With js-spawn, that is:

const count = await spawn(() => rows.filter((r) => r.score > threshold).length);

No second file. No postMessage. rows and threshold come along automatically. You get a Promise back, so await and try/catch work the way you already expect.

| | Raw Web Worker | js-spawn | | ---------------------- | ---------------------- | --------------- | | Extra file | yes | no | | Passing your variables | manual, by hand | automatic | | Getting the result | message listener | await | | Errors | onerror handler | try / catch | | Cleaning up the worker | you call terminate() | automatic |


Install

npm i js-spawn

Add the plugin to your Vite config:

// vite.config.ts
import { defineConfig } from 'vite';
import { jsSpawnVitePlugin } from 'js-spawn/plugin';

export default defineConfig({
  plugins: [jsSpawnVitePlugin()],
});

Restart the dev server. That's the whole setup.

The plugin does the real work: it finds every spawn() call at build time and turns the function into a real worker module. Without it, spawn() throws and tells you so.


Use it

import { spawn } from 'js-spawn';

Crunch numbers without blocking the UI

const numbers = Array.from({ length: 5_000_000 }, (_, i) => i);

const total = await spawn(() => numbers.reduce((a, b) => a + b, 0));

Sort a big list

const rows = await fetchRows(); // 100k rows

const sorted = await spawn(() => [...rows].sort((a, b) => a.price - b.price));

Parse something large

const csv = await file.text();

const records = await spawn(() =>
  csv.split('\n').map((line) => line.split(','))
);

Use your npm packages inside the worker

import { format } from 'date-fns';

const dates = ['2024-01-01', '2024-06-15'];

// `format` is imported into the worker for you
const labels = await spawn(() => dates.map((d) => format(new Date(d), 'PPP')));

Async works too

const data = await spawn(async () => {
  const res = await fetch('/api/report');
  return res.json();
});

Errors come back as rejections

try {
  await spawn(() => JSON.parse(brokenJson));
} catch (err) {
  console.error('failed in worker:', err.message);
}

What gets captured

Anything your function mentions from the surrounding file comes with it — variables and imports alike. You don't list them, and you don't pass arguments.

import { slugify } from './utils';

const prefix = 'post';
const titles = ['Hello World', 'Second Post'];

const slugs = await spawn(() =>
  // prefix, titles, and slugify are all captured
  titles.map((t) => `${prefix}-${slugify(t)}`)
);

Values are copied into the worker, not shared. Changing one inside the worker does not change it on the main thread:

const state = { count: 0 };

await spawn(() => {
  state.count += 1; // a copy
});

console.log(state.count); // still 0

Return what you want back out — that's the one channel that flows the other way.

Sharing memory on purpose

If you genuinely need both sides to see the same bytes, use a SharedArrayBuffer:

const shared = new SharedArrayBuffer(4);
const view = new Int32Array(shared);
view[0] = 1;

await spawn(() => {
  Atomics.add(new Int32Array(shared), 0, 41);
});

console.log(view[0]); // 42

What can't cross

Workers talk over structured clone, so a few things can't travel.

Fine to capture and return: strings, numbers, booleans, null, undefined, bigint, arrays, plain objects, Date, Map, Set, ArrayBuffer, SharedArrayBuffer, typed arrays.

Not allowed:

| Value | When you find out | | --------------------- | ----------------- | | functions and classes | at build time | | symbols | at runtime | | WeakMap / WeakSet | at runtime | | DOM nodes, window | at runtime | | most class instances | at runtime |

Runtime errors name the exact path that failed, like captured.user.avatar, so you don't have to guess.

Functions are the one that surprises people. A worker is a separate context — there is no way to send it a closure. So this fails:

const double = (n: number) => n * 2;

await spawn(() => [1, 2, 3].map(double)); // ❌ build error

Move the helper inside, and it's fine:

await spawn(() => {
  const double = (n: number) => n * 2; // ✅ lives in the worker
  return [1, 2, 3].map(double);
});

Imported functions are the exception — those are re-imported in the worker rather than copied, so import { format } from 'date-fns' works normally.


Workers are managed for you

You never create or terminate a worker.

  • Identical spawn() calls share one worker instead of each building their own.
  • A worker stays warm between calls, so bursts of work don't pay startup cost repeatedly.
  • Once it goes idle, it's terminated automatically.
  • A worker is never terminated while a task is still running.

Overlapping calls are safe — each await gets its own result back, even if they finish out of order.


Good to know

Server-side rendering. Workers don't exist on the server, so spawn() throws there. Call it on the client — inside an effect, an event handler, or after hydration.

Bundler support. Vite only, for now. Webpack, Rollup, and esbuild are planned.

When not to reach for this. Workers cost a few milliseconds to start and copy their inputs. For work that finishes in under ~10ms, or for anything that touches the DOM, stay on the main thread — this is for the genuinely slow stuff.


License

MIT © Mehran Taslimi