skip to content

Inside a dedicated Web Worker in the browser, why can you not touch the page's DOM, and which browser APIs are still available on the worker's global scope?

level: juniorimportance: must knowfreq 80%

answer

  1. separate thread, separate global
  2. not a second view of window
  3. DOM is single-threaded by design
  4. self is DedicatedWorkerGlobalScope
  5. fetch and indexedDB yes, localStorage no

basics

~20 s

A dedicated Web Worker runs on its own thread with its own global, DedicatedWorkerGlobalScope, so window, document and the DOM are absent — the DOM is not thread-safe. Workers still get fetch, WebSocket, indexedDB, timers and crypto.

solid answer

~40 s

A dedicated worker is a genuinely separate thread with its own global scope — an instance of `DedicatedWorkerGlobalScope`, reachable as `self` — rather than a second view of the page's `window`. The DOM is deliberately not exposed there: node trees, style and layout are designed for single-threaded access, and letting two threads mutate them concurrently would require locking every operation. So `document`, `window`, `alert`, `localStorage` and `sessionStorage` are simply not defined in a worker. What the worker does get is most of the non-UI platform: `fetch` and `XMLHttpRequest`, `WebSocket`, `indexedDB`, the `caches` interface, `crypto` including `crypto.subtle`, `performance`, `setTimeout`/`setInterval`, `console`, `navigator` (a `WorkerNavigator`), `location` (a `WorkerLocation`), `importScripts` in a classic worker, and `OffscreenCanvas` for off-thread drawing. Anything that must change pixels goes back to the page as a message and is applied there.

go deeper

for a junior

Be ready to say plainly that a worker runs on another thread with no window and no document, and that anything visual must be handed back to the page. Naming two or three APIs that do work there — fetch, indexedDB, timers — is enough.

for a middle

Explain the mechanism: a worker has its own global, a DedicatedWorkerGlobalScope reached as self, and the DOM is withheld because node trees and layout assume single-threaded access. Be able to sort a list of APIs into available and unavailable.

for a senior

Show you can judge what belongs in a worker at all — CPU-bound transformations with a small data surface, not DOM building — and that you know how to debug one, including its separate devtools context and the error event on the Worker object.

for a principal

Own the architectural line: a shared-nothing thread model buys you safety without locks but pays for it in data handoff, so the boundary between page and worker is a design decision about payload size and ownership, not a mechanical refactor.

## What a dedicated worker is `new Worker('/parse.js')` asks the browser to fetch a script and run it on a separate thread with a separate JavaScript realm. "Dedicated" means exactly one page owns it: the worker is reachable only from the document that created it, and it is destroyed when that document goes away. It is not a sandboxed copy of your page — it is a different global environment that happens to share the page's origin. That second realm has its own global object. In a document the global is a `Window`; in a dedicated worker it is a `DedicatedWorkerGlobalScope`. You reach it as `self` (and as `globalThis`), never as `window`. ```js // worker.js console.log(typeof window); // "undefined" console.log(typeof document); // "undefined" console.log(self === globalThis); // true ``` ## Why the DOM is missing The DOM is a mutable tree with a huge amount of derived state hanging off it: computed styles, layout boxes, the accessibility tree, selection, focus. Every one of those is maintained on the assumption that only one thread is reading and writing at a time. If a worker could call `element.remove()` while the main thread was mid-layout, the browser would need locks — or at least fine-grained synchronization — around essentially every DOM operation, which would slow down the common single-threaded case for everyone. The platform chose the other side of that trade: workers are *shared-nothing* by default. They do not see the page's objects at all; the two sides exchange copies of data as messages. Because nothing is shared, no locking is needed, and a worker can never corrupt the page's tree or leave it half-updated. The practical rule is: **a worker computes, the page renders.** ## What is actually available A worker global is far from empty. Typically present in a dedicated worker: - Networking: `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource` - Storage: `indexedDB`, `caches` - Timers and scheduling: `setTimeout`, `setInterval`, `queueMicrotask` - Crypto and encoding: `crypto` (including `crypto.subtle`), `TextEncoder`, `TextDecoder`, `atob`, `btoa` - Measurement and logging: `performance`, `console` - Environment: `navigator` (a `WorkerNavigator`, with members such as `navigator.userAgent` and `navigator.hardwareConcurrency`), `location` (a `WorkerLocation`) - Graphics without a DOM: `OffscreenCanvas`, `createImageBitmap` - Module loading: `importScripts` in a classic worker; static and dynamic `import` in a module worker Notably absent, besides the DOM itself: `alert`/`confirm`/`prompt`, `localStorage` and `sessionStorage` (synchronous storage is a `Window`-only API), and `document.cookie`. If a worker needs persistent storage it uses `indexedDB` or `caches`, both of which are asynchronous and worker-safe. ## Consequences for how you structure code The "no DOM" rule shapes what a worker is good for. Good candidates are self-contained transformations: parsing or diffing a large payload, running a search index, compressing an upload, decoding a binary format, image processing on an `OffscreenCanvas`. Bad candidates are anything whose real cost is DOM work — building thousands of elements, measuring layout, or animating — because that must happen on the main thread anyway. It also constrains libraries. A dependency that reaches for `window` or `document` at module-evaluation time will throw the moment you import it into a worker, even if the code path you wanted never touches the DOM. Feature-detect rather than assume: ```js const inWorker = typeof importScripts === 'function' || (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope); ``` ## Debugging Workers get their own execution context in browser devtools — a separate entry in the sources/thread list, with its own console output and its own breakpoints. If your `console.log` calls seem to vanish, you are almost certainly looking at the page's context instead of the worker's. Uncaught errors inside the worker also surface on the page as an `error` event on the `Worker` object, which is worth wiring up: ```js const worker = new Worker('/parse.js'); worker.addEventListener('error', (event) => { console.error('worker failed:', event.message, event.filename, event.lineno); }); ``` One more constraint that trips people up: the worker script URL must be same-origin with the page. You cannot point `new Worker()` straight at a third-party CDN; the usual workaround is to fetch the source yourself and construct the worker from a `blob:` URL you create.

  • If a worker cannot touch the DOM, how does work done there ever reach the screen?
    It doesn't directly. The worker sends its result back to the page as a message, and the page applies it to the DOM on the main thread. The one partial exception is `OffscreenCanvas`: the page can hand a canvas's drawing surface to the worker, which then paints into it off-thread without going through the DOM for each frame.
  • Why is localStorage unavailable in a worker when indexedDB is?
    `localStorage` is a synchronous API backed by shared per-origin state; exposing it to a second thread would mean blocking one thread on the other's writes and needing locking around every read. `indexedDB` and `caches` are asynchronous and transactional by design, so they can be used safely from multiple threads at once.
  • Can you point new Worker() at a script hosted on another origin?
    No — the worker script must be same-origin with the page, and CORS headers do not change that for the top-level worker script. The common workaround is to `fetch` the script text yourself, wrap it in a `Blob`, and pass the resulting `blob:` URL to `new Worker()`, which is treated as same-origin.

The worker is a back office, not a second cashier at the same till. It can do paperwork and phone the bank, but anything the customer actually sees has to be handed to the person at the counter.

saying these in an interview costs you the question

  • Says a worker gets its own copy of the DOM
  • Thinks a worker can update the UI directly if it is careful
  • Claims workers are just async callbacks, not a real thread
  • Assumes localStorage works inside a worker
  • Believes window exists in a worker but is empty

context