# WorkerDOM: running the DOM inside a Web Worker

> WorkerDOM lets a section of a page be driven by a Web Worker, with DOM mutations sent to the main thread as messages. It is aimed at third-party embeds and heavy rendering, and its API coverage is deliberately partial.

**ampproject/worker-dom** — The same DOM API and Frameworks you know, but in a Web Worker.

- Repository: https://github.com/ampproject/worker-dom
- Stars: 3,264 · Forks: 155
- Language: TypeScript
- License: Apache-2.0
- Published: 2026-09-24 · Updated: 2026-09-24 · Language: en
- Canonical page: https://hysenlabs.com/projects/ampproject-worker-dom

## The problem WorkerDOM targets: moving DOM work off the main thread

Browsers give JavaScript one main thread for layout, style and script. A Web Worker runs script in parallel but has no DOM. WorkerDOM closes that gap by providing a DOM implementation inside the worker, so code that would normally touch document can run off the main thread. The README states the purpose plainly: move the complexity of intermediate work related to DOM mutations to a background thread and send only the necessary manipulations to a foreground thread. The intended audience is not every web app. The README lists three use cases: embedded content from a third party living beside first party code, mitigation of expensive rendering for content that does not need synchronous updates to user actions, and keeping the main thread available for high priority updates by updating elsewhere in the document asynchronously. All three assume the worker-driven region is a bounded part of a page, not the whole application. That framing matters when you evaluate it. WorkerDOM is not a way to move an entire SPA off the main thread, and the README never claims that. It is a way to isolate a subtree.

## How the main thread and worker thread split the DOM

The architecture is a two-sided implementation. The main thread keeps a mirror of the real DOM and applies mutations; the worker thread holds a DOM facade that scripts interact with. When worker code changes a node, the change is serialized and posted to the main thread, which performs the corresponding operation on the real document. The repository layout reflects this split: src/ contains main-thread and worker-thread source trees, each with its own tsconfig, and the build produces separate main and worker bundles. The README describes two distribution flavours, a global variant and a module variant, and says the main thread code can be included directly in the document or via a bundler. There is also a special amp output variant under amp/main.mjs and amp/worker/worker.mjs that supplies extra hooks for safety features such as HTML sanitization, and a debug variant under debug/ that includes additional debugging messages. The amp variant assumes the consumer will compile the distributed JavaScript to support older user agents, which tells you the raw output is not written for the oldest browsers on its own. The demos in demo/ show the range the maintainers exercise, including preact-todomvc, react-map, vue-todomvc, shadow-dom, svg and canvas.

## Installing WorkerDOM and upgrading a real element

The package is published to npm as @ampproject/worker-dom, and package.json requires Node 18 or later for local development. Install it first.

```bash
npm install @ampproject/worker-dom
```

The README's usage example starts from a plain div in the page that carries a src attribute and an id. That element is the region that gets handed to the worker.

```html
<div src="hello-world.js" id="upgrade-me"></div>
```

To upgrade it, import upgradeElement from the module build and pass the element plus the path to the worker bundle. The README shows exactly this shape.

```html
<script type="module">
  import {upgradeElement} from './dist/main.mjs';
  upgradeElement(document.getElementById('upgrade-me'), './dist/worker/worker.mjs');
</script>
```

If you cannot use modules, the nomodule build exposes a global called MainThread, and the README upgrades the same div by calling MainThread.upgradeElement inside a DOMContentLoaded listener, passing the element and './dist/worker/worker.js'. After either call, the worker owns that subtree. To try the project without wiring it into your own app, the repository ships demos: clone it, run npm run demo, and the script builds the current version and starts a local webserver on port 3001.

## Partial API coverage is the main constraint

The README is direct about coverage: most DOM elements and their properties are supported, DOM query APIs like querySelector have partial support, and browser APIs like History are not implemented yet. It points to web_compat_table.md in the repository for the support matrix. That table, not the README, is the document you have to read before committing to WorkerDOM, because partial support on querySelector affects a large amount of ordinary code. A framework that queries the tree by selector, or a script that depends on History, will hit the boundary quickly. The project also describes itself as an in-progress implementation, and the release history is uneven: v0.36.0 is dated 2025-06-27, while the two releases before it, v0.33.0 and v0.34.0, are dated February and March 2022. The last push to the default branch was on 2026-09-23. Treat the version number as pre-1.0 and expect the API surface to keep shifting. The README does not document a rollback path for an upgraded element, so if you need to return a subtree to main-thread control, that is not covered in the project's own documentation.

## Where a plain Web Worker or an iframe is the better fit

The closest alternative is a plain Web Worker with no DOM at all. That approach is simpler and has no compatibility table to consult: you compute in the worker, post structured data back, and let main-thread code do every DOM write. The difference in approach is where the DOM model lives. WorkerDOM maintains a DOM facade inside the worker so existing framework code can run there with few changes, at the cost of serializing mutations and tracking a mirrored tree. A plain worker gives you no DOM API and forces you to write the message protocol yourself. If your goal is only to move computation, such as parsing or number crunching, the plain worker is the smaller dependency. If your goal is to run third-party code that expects document, WorkerDOM is the one that offers that facade. An iframe is the other real alternative, and it isolates third-party content at the browser level rather than at the thread level, which is a different security and layout story altogether. The README's own use case list, third-party embedded content beside first party code, is exactly the ground where the iframe comparison is worth making before you pick.

## Licence and the cost of keeping up

WorkerDOM is licensed under the Apache License, Version 2.0, and the LICENSE file sits at the repository root. The package.json declares the same identifier, so the npm package and the repository agree. Apache-2.0 includes a patent grant and requires that notices be preserved, but this is not legal advice and you should have counsel review distribution terms if you ship the amp or debug variants. On upgrade cost, the practical burden is the compatibility table. Because query coverage is partial and the API is pre-1.0, an upgrade can change which selectors or properties work, and the release log linked from the README is the only changelog the project documents. There is no separate migration guide in the repository layout. If you pin a version, plan to re-read web_compat_table.md at each bump rather than assuming the surface only grows. The debug build under debug/ is the cheaper way to see what the worker is doing when a mutation does not appear on the page.

## Conclusion

WorkerDOM suits teams embedding third-party or heavy rendering into an existing page and willing to work inside its partial DOM surface. It is the wrong tool if you need full querySelector coverage, History, or a stable API, since the README calls the project an in-progress implementation. Before adopting, check web_compat_table.md for the exact elements and properties you depend on, and confirm your build can consume dist/main.mjs plus dist/worker/worker.mjs.

## FAQ

### What is WorkerDOM and what does it do?

WorkerDOM is an in-progress implementation of the DOM API intended to run within a Web Worker. It moves DOM mutation work to a background thread and sends only the necessary manipulations to the foreground thread.

### How do I install WorkerDOM?

Install it from npm with npm install @ampproject/worker-dom. The README then shows including either dist/main.mjs as a module or dist/main.js with nomodule defer in your document.

### Which JavaScript APIs can I use with WorkerDOM?

The README says most DOM elements and their properties are supported, DOM query APIs like querySelector have partial support, and browser APIs like History are not implemented yet. The API support matrix is in web_compat_table.md.

### How do I run the WorkerDOM demos locally?

After cloning the repository, run npm run demo. The README states this builds the current version and starts a local webserver on port 3001.

### Which browsers does WorkerDOM support?

The README says the project supports the latest two versions of major browsers including Chrome, Firefox, Edge, Safari, Opera and UC Browser, across desktop, phone, tablet and web view. It also aims to keep IE 11, iOS 8, the Android 4.0 system browser and Chrome 41 from breaking.

## Sources

- [ampproject/worker-dom on GitHub](https://github.com/ampproject/worker-dom)
- [Issues](https://github.com/ampproject/worker-dom/issues)
- [License: Apache-2.0](https://github.com/ampproject/worker-dom/blob/main/LICENSE)
- [README](https://github.com/ampproject/worker-dom/blob/main/README.md)
- [Releases](https://github.com/ampproject/worker-dom/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ampproject-worker-dom
