A page calls navigator.serviceWorker.register('/sw.js') on a visitor's first visit, and that worker has a fetch handler. Does the worker intercept the requests of the page load that registered it? Explain what decides whether a document is controlled.
answer
- controller decided at navigation time
- first visit has no active worker
- registering does not intercept retroactively
- clients.claim() adopts uncontrolled pages
basics
~20 sNo. A document's controller is chosen when it is navigated to, and on a first visit no worker is active yet, so navigator.serviceWorker.controller is null and every request on that load goes straight to the network.
solid answer
~40 sNo. `register()` only starts the lifecycle: the browser downloads the script, runs `install`, then `activate`, and by the time the worker is activated the page has already been navigated and its resources fetched. A document's controller is fixed at navigation time, so a page that was loaded with no active worker stays uncontrolled for its whole life — `navigator.serviceWorker.controller` is `null` and the `fetch` handler never sees its requests. The next navigation inside the registration's scope is controlled, which is why the common phrasing is "the service worker takes effect on the second load". If you want the registering page itself to become controlled, the worker must call `self.clients.claim()` in its `activate` handler; the page then fires `controllerchange`, and requests it makes *after* that point go through the worker.
go deeper
Be able to say plainly that the worker takes effect from the next navigation, and that navigator.serviceWorker.controller is null on the load that registered it.
Explain the ordering: register triggers download, install, then activate, all after the page has already fetched its resources, and that a document's controller is fixed at navigation time.
Show you can diagnose half-controlled sessions in production — knowing that clients.claim() only affects requests made after the claim, and that a hard reload loads the page uncontrolled, keeps you from misreading bug reports.
Own the decision of whether to claim at all: claiming makes the first session consistent but means a page loaded from the network is suddenly served by a worker, so decide it alongside your activation and versioning policy.
## The two things people conflate A service worker registration has a **lifecycle** (installing → installed → activating → activated → redundant) and separately each open document has a **controller** — the worker, if any, that gets `fetch` events for that document's requests. Registering a worker advances the lifecycle. It does not retroactively hand an already-loaded document a controller. ## What register() actually does ```js const reg = await navigator.serviceWorker.register('/sw.js'); ``` The browser resolves the script URL, checks it is same-origin and in a secure context (HTTPS, or `localhost` for development), downloads it, and if it is new it creates a worker in the *installing* state. The `install` event fires; when it finishes the worker becomes *installed*. If no other worker is active for that registration, it moves straight on to *activating*, fires `activate`, and becomes *activated*. All of that is asynchronous and typically completes after the page has already finished loading its HTML, CSS and scripts from the network. ## The controller is chosen at navigation time When the browser navigates to a URL, it looks for a registration whose scope matches that URL and which already has an **active** worker. If it finds one, the new document is created *controlled* by that worker, and from then on the worker receives a `fetch` event for the document's subresource requests and for later `fetch()`/`XMLHttpRequest` calls. If there is no active worker at navigation time, the document is created **uncontrolled**, and that is permanent for the document unless the worker claims it. This is why the very first visit — the one that installs the worker — is never intercepted. You can observe it directly: ```js navigator.serviceWorker.register('/sw.js').then((reg) => { console.log(navigator.serviceWorker.controller); // null on the first visit console.log(reg.active); // may be non-null moments later }); ``` Note the asymmetry: `reg.active` can be a worker while `navigator.serviceWorker.controller` is still `null`. The registration has an active worker; *this document* simply is not one of its clients. ## clients.claim() The escape hatch is `self.clients.claim()`, called from the worker's `activate` handler: ```js self.addEventListener('activate', (event) => { event.waitUntil(self.clients.claim()); }); ``` This makes the newly activated worker take control of in-scope clients that currently have no controller. Each such page fires `controllerchange` on `navigator.serviceWorker`, and `navigator.serviceWorker.controller` becomes non-null. Be honest about what this buys you: the page's HTML, CSS and initial scripts were already fetched from the network before the claim happened, so claiming does not retroactively serve them from the worker. It only affects requests the page makes *after* it is claimed — lazily loaded chunks, API calls, images added later. Its real value is making the first session behave consistently instead of half-controlled. ## How to tell what state you are in - `navigator.serviceWorker.controller` — `null` means this document is uncontrolled. - `navigator.serviceWorker.ready` — resolves with a registration that has an active worker. It does **not** promise that this document is controlled; a first-load page can have `ready` resolve while `controller` is still `null`. - `navigator.serviceWorker.addEventListener('controllerchange', …)` — fires when the document's controller changes, e.g. after a claim or after a new worker skips waiting. ## Traps worth knowing - **Hard reload bypasses the worker.** A shift-reload (or "Empty cache and hard reload") loads the page uncontrolled on purpose, which is why a bug "disappears" for the developer testing it that way. - **Secure context required.** Registration only works over HTTPS or on `localhost`; on plain HTTP over a network the promise rejects. - **Scope matters.** A page outside the registration's scope is never controlled, no matter how many times it registers the worker. - **Registering repeatedly is cheap.** Calling `register()` with the same URL on every page load is the normal pattern; if the byte content is unchanged the browser keeps the existing registration.
- Can you make the very first load benefit from the worker without a second navigation?Only partially. `self.clients.claim()` in `activate` takes control of the already-open, uncontrolled page and fires `controllerchange`, but the document's HTML and initial subresources were fetched before that moment. Only later requests — lazy chunks, API calls, images — go through the worker. If you genuinely need the whole first load handled, you have to reload after `controllerchange`, which costs the user a visible reload.
- Does navigator.serviceWorker.ready resolving mean the current page is controlled?No. `ready` resolves with a registration that has an *active* worker for the page's scope. That is a property of the registration, not of this document. On a first visit `ready` can resolve while `navigator.serviceWorker.controller` is still `null`. To know whether this document is controlled, check `controller` or listen for `controllerchange`.
- Why does the bug you are chasing disappear when you hard-reload the page?A hard reload (shift-reload) deliberately loads the document without a controller and bypasses the worker entirely, so responses come from the network. That makes it a good confirmation that the worker is the cause, but a bad way to test a fix — use a normal reload, or DevTools' Application panel, to exercise the controlled path.
saying these in an interview costs you the question
- Thinks register() immediately intercepts the current page's requests
- Believes the fetch handler runs during the install event
- Says any reload switches the page's controller
- Claims service workers work over plain HTTP on any host
- Confuses navigator.serviceWorker.ready with the page being controlled