Browser
knitting/browser runs the same pool API on web workers and SharedArrayBuffer.
Tasks, typed-array payloads, parallel calls, abort signals, and shutdown all work the
way they do on Node, Deno, and Bun.
This guide covers browser setup, unsupported features, and the errors you may encounter.
The browser smoke test runs the hosted bundle in your
own browser. It starts real one-thread pools through both task() and importTask(),
so you can watch the build work before you install anything.
Two things your page must do
Section titled “Two things your page must do”Serve the page cross-origin isolated
Section titled “Serve the page cross-origin isolated”The page needs cross-origin isolation to use SharedArrayBuffer. Set these
headers:
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corpWithout them, createPool throws before it starts a worker:
SharedArrayBuffer is unavailable: serve the page cross-origin isolated (Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: require-corp).
Knitting requires shared memory, so the pool cannot start without cross-origin isolation.
Turning isolation on affects the whole page, not just Knitting. Every cross-origin
image, script, or font now needs Cross-Origin-Resource-Policy or CORS, or the
browser refuses to load it.
This site sends both headers in local Astro dev and preview, and on Netlify through
public/_headers. If you host the bundle somewhere else, set them up there too. When
the smoke test reports crossOriginIsolated: false, a missing header is almost always
the reason.
Call setModuleUrl(import.meta.url) in every task module
Section titled “Call setModuleUrl(import.meta.url) in every task module”A worker has to import the module your tasks live in, so Knitting needs that module’s
URL. On Node, Deno, and Bun it finds the URL by reading the call stack. That is not
reliable in browsers: it depends on Error.prepareStackTrace, which only V8 provides,
and a bundler rewrites the paths anyway. In a browser, register the module URL
explicitly:
import { setModuleUrl, task } from "knitting/browser";
setModuleUrl(import.meta.url);
export const square = task({ f: (value) => value * value });Put the call at the top, above the task() and importTask() calls it covers. Leave
it out and the first of those calls throws, long before you reach a pool:
Unable to determine caller file. This runtime exposes no stack traces (e.g. Andromeda); call setModuleUrl(import.meta.url) at the top of the module that defines your tasks before creating a pool.
An unbundled page in Chromium works without the call, because there the stack really does name the module. Do not rely on that. It breaks as soon as the page goes through a bundler or opens in Firefox or Safari.
Basic URL import example
Section titled “Basic URL import example”You do not need a package import in a browser. Load the bundle from a URL, then point
importTask() at a second URL that holds your tasks.
const knittingUrl = new URL("/knitting.js", window.location.origin).href;const taskModuleUrl = new URL("/example-task.mjs", window.location.origin).href;const { importTask } = await import(knittingUrl);
const add = importTask<[number, number], number>({ href: taskModuleUrl, name: "add",});
const pool = add.createPool({ threads: 1 });
try { console.log(await pool.call([2, 3])); // 5} finally { await pool.shutdown();}The task module also registers its URL:
import { setModuleUrl, task } from "./knitting.js";
setModuleUrl(import.meta.url);
export const add = task({ f: ([a, b]) => a + b,});With this setup, the page loads knitting.js by URL, each task module
reports its own URL, and the workers import from there.
What a page cannot do
Section titled “What a page cannot do”| Feature | What happens |
|---|---|
Process workers (worker.runtime: "process", processRuntime) | throws process workers are unavailable in the browser build |
Compiled / Porffor workers (runtime: "compiled", .knt artifacts) | throws compiled workers are unavailable in the browser build |
BufferReference | throws BufferReference cannot run in runtime "browser" |
ProcessSharedBuffer, named shared memory | throws ProcessSharedBuffer is unavailable in the browser build |
| Native addons, FFI, file descriptors | unreachable; nothing in a page can load them |
checkCompiledWorker | not exported from knitting/browser |
Permissions (permission: {...}) | accepted and ignored, see below |
All of these need a filesystem, a process to spawn, or FFI. The browser build replaces them with stubs that throw the messages above, so a call fails where you wrote it instead of somewhere deep inside a worker.
Permissions do nothing here
Section titled “Permissions do nothing here”The permission option is accepted and then skipped. There is no filesystem to
restrict, no process to sandbox, and no runtime flags to pass, so a policy that locks
down a Node worker locks down nothing in a page.
Passing a SharedArrayBuffer yourself
Section titled “Passing a SharedArrayBuffer yourself”Handing a SharedArrayBuffer to a task as an argument works on Node, Deno, and Bun.
In a browser it throws:
KNT_ERROR_3: Unsupported payload type; BufferReference cannot run in runtime "browser"That path shares a buffer by pinning a pointer through FFI, and a page has no FFI. The pool’s own transport is unaffected, which is how the workers talk at all. Only buffers you pass yourself are refused.
A browser version of this is possible. A cross-origin isolated page can send a
SharedArrayBuffer straight through postMessage, with no pointer involved. It is
just not implemented yet.
Works, but differently
Section titled “Works, but differently”- Workers start from a message, not from
workerData. The pool posts the boot payload after it constructs the worker. You cannot see this from the API; it matters if you are reading worker startup code. KNITTING_DEBUGdoes nothing. The env gate readsDeno.envorprocess.env, and a page has neither. Pass thedebugoption tocreatePoolinstead.threadsstill defaults to 1. No runtime picks a thread count for you. In a browser,navigator.hardwareConcurrencytells you how many cores you have.- Every worker parses the whole bundle. The worker URL is the bundle’s own URL, so memory use grows with thread count.
Not a browser problem
Section titled “Not a browser problem”These fail the same way on Node, so do not go looking for a browser cause:
BigIntpayloads:Do not know how to serialize a BigIntMapandSetpayloads:Unsupported object type- Functions as payloads:
KNT_ERROR_0: Function is not a valid type Errorvalues round-trip as{ name }, dropping the message
Browser support
Section titled “Browser support”Tests run against headless Chromium in two layouts: one bundle holding tasks and library together, and the standalone single-file bundle loaded from a script tag beside a separate task module.
Chromium is the only engine covered. Firefox and Safari have the same building blocks,
web workers and SharedArrayBuffer under cross-origin isolation, so they are expected
to work, but nothing has been checked against them. One difference is already known:
neither implements Error.prepareStackTrace. There, setModuleUrl(import.meta.url)
is not just good practice, it is the only way a task module can be found.
How the build is produced
Section titled “How the build is produced”knitting/browser ships as a single self-contained file. The copy hosted here is
the published 0.1.70 browser build, about 113 KB on disk and 38 KB gzipped.
Install knitting from npm when you need a versioned local copy.
Node-only subsystems are swapped for stubs at bundle time by
scripts/browser-stubs/plugin.ts.
Each stub does what the real module would have done in a page anyway: return nothing,
answer false, or throw one of the messages above. That is why these errors name the
feature you reached for instead of failing generically.