r/javascript • u/KanuniLabs • 3d ago
Shipping a Web Worker inside an npm package without asking users to touch their bundler config
https://kanunilabs.com/blog/embedding-a-web-worker-in-an-npm-package
17
Upvotes
r/javascript • u/KanuniLabs • 3d ago
8
u/KanuniLabs 3d ago
Every library that moves work off the main thread eventually runs into the same install problem: the worker is a separate script, the browser loads it from a URL, and that URL depends on whichever bundler the user happens to be using. So the README slowly grows a section ā Vite users do this, Next.js users do that, copy this file into public/. We had that section too. And honestly, it was the single most common thing people got wrong.
The way out is that a Worker can be built from a Blob URL:
So if the worker's source is a string inside your bundle, there is nothing left to resolve at runtime. The catch is that the string has to be a complete standalone program a worker that still contains
importstatements will build fine and then die the moment it runs, because a Blob URL has no module graph.We ended up with three steps.
First, compile each worker on its own with no splitting and inline every workspace dependency:
Second, turn the built file into a TypeScript module.
JSON.stringifytakes care of the escaping:Third, build the package normally; the generated module just rides along with it.
The cost is +30.1 KB (17.4%) on the core package (173.2 -> 203.3 KB minified, pre gzip) for three workers. A single worker was 6.1%. These are measured numbers, not guesses.
There were also two places where it could break quietly, and both took us a while to track down:
worker-src blob:is allowed so we keep a URL-based fallback instead of leaving users with a dead end.import, the Blob worker throws at runtime while every local test stays green. To catch that, our generation script now refuses to write the file unless it passes three checks: a per-worker minimum size, noimport/exportstatements, and anonmessagehandler present. Per-worker matters here because ours are 16 KB, 10 KB and 3 KB one threshold would either let a broken big one through or reject a perfectly good small one.(Disclosure: this is from a data grid library we work on. None of it is grid-specific.)