| layout | docs |
|---|---|
| docsOrder | 110 |
| handlebars | false |
Use page-scoped web workers to move work off the browser's main thread, and a site service worker to control requests and caching. The DOMStack manifest provides an inventory of built files for service-worker policies and other tooling.
[[toc]]
You can write dedicated workers and shared workers for a page by adding a file called ${name}.worker.ts or ${name}.worker.js, where name becomes the key for the worker filename in workers.json.
DOMStack builds both worker kinds as ESM entry points similarly to page client.ts bundles and can split shared dependencies into chunks with the rest of your site.
page-directory/
├── page.js
├── client.js
├── counter.worker.js # Worker with counter functionality
└── data.worker.js # Worker for data processing
To use a worker, load the generated ./workers.json file to get the content-hashed worker entrypoint filename and then create a Worker or SharedWorker with that filename.
// First, fetch the workers.json to get worker paths in your client.ts
async function initializeWorkers() {
const response = await fetch('./workers.json');
const workersData = await response.json();
// Initialize workers with the correct hashed filenames
const counterWorker = new Worker(
new URL(`./${workersData.counter}`, import.meta.url),
{ type: 'module' }
);
// Use the worker
counterWorker.postMessage({ action: 'increment' });
counterWorker.onmessage = (e) => {
console.log(e.data);
};
return counterWorker;
}
const worker = await initializeWorkers();A shared worker uses the same generated mapping, but clients communicate through its port property.
All pages connecting to a shared worker must use the same origin and worker URL.
const response = await fetch('./workers.json')
const workersData = await response.json()
const sharedWorker = new SharedWorker(
new URL(`./${workersData['shared-counter']}`, import.meta.url),
{ type: 'module' }
)
sharedWorker.port.onmessage = event => {
console.log(event.data)
}
sharedWorker.port.start()
sharedWorker.port.postMessage({ action: 'increment' })Inside shared-counter.worker.ts, accept each connection and communicate through the supplied MessagePort.
/// <reference lib="webworker" />
const sharedWorker = globalThis as unknown as SharedWorkerGlobalScope
let count = 0
sharedWorker.onconnect = event => {
const port = event.ports[0]
if (!port) return
port.onmessage = message => {
if (message.data.action === 'increment') count++
port.postMessage({ count })
}
port.start()
port.postMessage({ count })
}See the Web Workers Example for a complete implementation.
DOMStack has full native support for service workers.
Put one site service worker source file anywhere under src and domstack will build it to a stable
root /service-worker.js output:
src/
└── globals/
└── service-worker.tsDOMStack produces:
public/
└── service-worker.jsNote
Wherever service-worker.ts is used, you can also use service-worker.js.
Type checking is supported in both file types.
See Supported file types for all available extensions.
Only one site service worker source is allowed.
If multiple service-worker.* sources are present,
domstack fails with DOM_STACK_ERROR_DUPLICATE_SERVICE_WORKER.
Service workers are bundled using the project’s esbuild.settings.ts configuration, so imports work the same way they do for client bundles and page-scoped web workers.
The
entry filename is intentionally not content-hashed because browser service-worker update checks need
a stable URL.
DOMStack provides the service-worker URL and scope to browser bundles through esbuild define values:
| Define | Value |
|---|---|
process.env.DOMSTACK_SERVICE_WORKER_URL |
Public URL of the site service worker, usually /service-worker.js, or "" when no service worker is present |
process.env.DOMSTACK_SERVICE_WORKER_SCOPE |
Registration scope for the site service worker, usually /, or "" when no service worker is present |
Register the built service worker from your site client code, usually global.client.ts:
// src/globals/global.client.ts
const serviceWorkerUrl = process.env.DOMSTACK_SERVICE_WORKER_URL
const serviceWorkerScope = process.env.DOMSTACK_SERVICE_WORKER_SCOPE
if (serviceWorkerUrl && serviceWorkerScope && 'serviceWorker' in navigator) {
navigator.serviceWorker.register(serviceWorkerUrl, {
scope: serviceWorkerScope,
type: 'module',
updateViaCache: 'none'
})
}DOMStack does not inject this into the default layout. Registration timing, update prompts, development opt-outs, and recovery behavior are application policy, so keep that logic in your global client or an imported client module.
Browsers allow service-worker registration only in a secure context, normally HTTPS in production or localhost during development.
The service-worker script must be served from the same origin as the page.
DOMStack emits it at the origin root so its default scope can cover the entire site.
Register it with type: 'module' because DOMStack builds the worker as ESM.
A Web App Manifest is not required to register or run a service worker. Add one when the site also needs installable-app metadata such as its name, icons, start URL, display mode, and theme colors. DOMStack does not generate this browser manifest. Author it as a static asset and reference it from the document head:
<!-- HTML generated by src/layouts/root.layout.ts -->
<link rel="manifest" href="/site.webmanifest">See these complete examples:
static-mpa-offlineuses DOMStack's manifest hooks with a custom service worker and registration lifecycle.static-mpa-workbox-offlineimplements the same offline MPA pattern with Workbox.
Caution
DOMStack does not clean dest before building.
Clean the destination before deployment, especially after removing or renaming a service worker, so an old /service-worker.js cannot remain publicly available.
The DOMStack manifest is build metadata for service workers, deployment tools, and other build-time integrations. See Output manifest for configuration, hooks, and service-worker integration.