skip to content

For a single-page app that lazily loads content-hashed JavaScript chunks, would you have the service worker call self.skipWaiting() on every install? Lay out the tradeoff against leaving the new worker waiting and prompting the user to reload.

level: principalimportance: should knowfreq 35%

answer

  1. one document, one serving version
  2. hashed chunk URLs are a build contract
  3. immediacy versus session consistency
  4. prompt, then reload on controllerchange
  5. guard against reload loops

basics

~20 s

Usually not. skipWaiting() activates the new worker under pages that were loaded from the previous build, so a running page can ask for chunk URLs the new worker no longer knows about. For a chunked SPA, prefer leaving it waiting and prompting the user to reload.

solid answer

~60 s

`skipWaiting()` is not free: it activates the new worker immediately and hands it the clients the old worker was controlling. A page loaded from build A is now being served by build B's worker, and a lazily loaded chunk requested after that point is a build-A hashed URL that build B may have already cleaned up — you get a 404 or a chunk-load error mid-session, exactly the kind of failure users cannot self-diagnose. Leaving the worker waiting preserves the invariant that one document is served by one build for its whole life. The cost is latency: the fix reaches long-lived tabs only when the user reloads, so you pair waiting with an "update available" prompt that signals the worker to skip waiting and reloads on `controllerchange`. I would take unconditional `skipWaiting()` only where there is no cross-request version contract — a mostly static content site, or a worker whose `fetch` handler is a version-agnostic pass-through — or in an emergency, where getting a broken build out beats a clean session.

go deeper

for a junior

Know what skipWaiting() does — the new worker activates without waiting for tabs to close — and that it is a decision, not a default to copy.

for a middle

Explain the mechanism of the skew: skipWaiting hands the new worker the old worker's clients, so a page from the previous build can request asset URLs the new build no longer serves.

for a senior

Be able to build the alternative end to end: detect the waiting worker, prompt, signal it, reload once on controllerchange with a loop guard, and describe the failure modes each guard prevents.

for a principal

Own it as a policy spanning activation timing, asset retention, deploy cadence and the version telemetry that tells you when 99% of sessions have moved — and be explicit about which failure you are choosing to accept.

## What the choice actually is The platform's default is conservative: a new worker installs, then waits until the old one has no clients. That default encodes an invariant — **a document is served by the worker version that was active when it was navigated to**. `skipWaiting()` trades that invariant for immediacy: activate now, take over the pages the old worker controlled, fire `controllerchange` in them. So the question is not "is skipWaiting good", it is "does my app have a cross-request version contract that breaks when the serving version changes mid-document". ## Why chunked SPAs break A build emits `main.a1b2c3.js` plus lazy chunks like `settings.d4e5f6.js`. The chunk names are baked into the entry bundle at build time. A user opens the app on build A and leaves the tab open. You deploy build B; the worker installs and, with `skipWaiting()`, activates under the still-running build-A page. The user then navigates to Settings, the build-A entry bundle asks for `settings.d4e5f6.js`, and the build-B worker is now in charge of answering. If the new worker has cleaned up the previous build's stored assets and the origin no longer serves that hash, the request fails and the route fails to load. The user sees a blank panel or a chunk-load error, refreshes, and it works — an intermittent bug that is almost impossible to reproduce from a bug report. The same class of skew appears anywhere the client and the worker share an assumption: request/response shapes the worker rewrites, a storage schema the new worker migrates on activate, or asset URL conventions that changed between builds. ## The waiting-plus-prompt pattern ```js // page const reg = await navigator.serviceWorker.register('/sw.js'); function promptForUpdate(worker) { showToast('A new version is available', () => { worker.postMessage({ type: 'SKIP_WAITING' }); }); } if (reg.waiting && navigator.serviceWorker.controller) promptForUpdate(reg.waiting); reg.addEventListener('updatefound', () => { const incoming = reg.installing; incoming.addEventListener('statechange', () => { if (incoming.state === 'installed' && navigator.serviceWorker.controller) { promptForUpdate(incoming); } }); }); let reloading = false; navigator.serviceWorker.addEventListener('controllerchange', () => { if (reloading) return; reloading = true; window.location.reload(); }); ``` The worker calls `self.skipWaiting()` when it receives that message. Three details that separate a working implementation from a broken one: - **Guard the reload.** Without the `reloading` flag, a `controllerchange` that fires for another reason can put the tab in a reload loop. Also skip the reload when `navigator.serviceWorker.controller` was `null` at startup — that is a first install claiming the page, not an update. - **Only prompt on a real update.** `installed` plus an existing controller means an update is waiting; `installed` with no controller is a first install and there is nothing to tell the user. - **Make the reload the moment everything changes.** The user consented to a discontinuity, so use it: new worker, new document, new assets, all at once. ## When unconditional skipWaiting is the right call - **No version contract across requests.** A content site whose worker serves documents and images with no build-coupled URL scheme has nothing to skew. - **Version-agnostic worker.** If the `fetch` handler is a thin network-first pass-through that does not depend on which build produced the request, activation timing barely matters. - **Emergencies.** When the currently active worker is the bug, an immediate takeover is better than a clean session; that is the kill-switch case. - **Ephemeral sessions.** If users never keep a tab open across a deploy, the risk is theoretical — but verify that with data rather than assuming it. ## The organisational half of the decision This choice interacts with things outside the worker: how often you deploy, whether old build artifacts stay served for a grace window, and whether the new worker deletes the previous build's stored assets on activate or keeps them for one generation. A team that keeps the last two builds' assets reachable can afford a much more aggressive activation policy than one that cleans up immediately. Decide those together — activation policy, asset retention, and deploy cadence are one system, and the failure only shows up where they disagree. Whatever you choose, make it observable: report the build version from both the page and the worker so you can measure how many sessions are running mismatched versions, and set an expectation for how quickly a fix must reach the last user. "How long until 99% of sessions are on the new worker?" is the number this decision is really about.

  • How do you keep the reload-on-controllerchange handler from looping?
    Set a boolean before calling `location.reload()` and return early if it is already set, and skip the reload entirely when `navigator.serviceWorker.controller` was `null` at page start — that case is a first install claiming the page, not an update. Without both guards, a claim or an unexpected controller change can put the tab into a reload cycle that users cannot escape.
  • Does keeping the previous build's assets reachable change the calculus?
    Substantially. Most of the skew risk is that a build-A page asks for a build-A URL that no longer exists. If your deploy keeps the previous one or two builds' artifacts served, and the new worker does not delete them on activate, an immediate takeover becomes much safer. Retention policy and activation policy should be decided together.
  • What would you measure to know whether your activation policy is working?
    Two things: the distribution of sessions by build version over time after a deploy — how long the tail takes to drain — and the rate of chunk-load or asset 404 errors broken down by whether the page's build matches the controlling worker's. The first tells you whether fixes reach users fast enough; the second tells you whether you are buying that speed with broken sessions.

saying these in an interview costs you the question

  • Calls skipWaiting unconditionally because a tutorial did
  • Thinks skipWaiting only affects newly opened tabs
  • Believes waiting means users never get the update
  • Reloads on controllerchange with no loop guard
  • Treats activation policy as unrelated to asset retention

context