Skip to content

pwa(sw): a second tab loading during another tab's first-ever install can still see one unwanted reload #614

Description

@qnbs

Context

Found during PR #613's review (fixing #585 — the SW controllerchange handler no longer reloads on an ordinary first-ever install). A narrower, compound-timing residual remains that even a fully-corrected live Service Worker API check cannot close.

The scenario

register-sw.ts's registerServiceWorker() runs via window.addEventListener('load', registerServiceWorker) — i.e. at the load event, not at navigation time. Its hadControllerAtLoad snapshot (!!navigator.serviceWorker.controller) is read at that point.

If Tab A is the first tab to ever visit an origin and its install→activate→clients.claim() sequence completes while Tab B (also loading for the very first time) is still parsing/loading its own page, clients.claim() sets Tab B's .controller property immediately — before Tab B's own load event fires, and therefore before Tab B's own registerServiceWorker() (and its controllerchange listener) ever runs.

Two independent consequences:

  1. Tab B's controllerchange event (from Tab A's claim) has no listener yet when it's dispatched — it is not replayed, so it's silently missed (same class as the listener-ordering gap PR fix(pwa): skip the controllerchange reload on a first-ever install (#585) #613 already closed elsewhere, but this instance happens before registerServiceWorker() even starts).
  2. When Tab B's load handler eventually runs, hadControllerAtLoad reads true — because .controller was already set by Tab A's claim — even though this is still, in reality, the very first activation this origin has ever had. Tab B's classification logic (correctly, given only this signal) concludes "this is a genuine update," so if Tab B's page code ever reacts to that state, it treats what was actually the shared first-ever install as if it were a returning-visitor update.

Why not fixed in #613

Closing this fully requires a signal that doesn't depend on live, per-tab Service Worker API timing at all — e.g. a persistent flag (localStorage, written the moment any tab first observes a genuine claim) checked synchronously before any Service Worker API call. That is a materially different design from the live-registration-state approach #613 already converged on through several rounds of narrower fixes, and disproportionate to add speculatively without a concrete reproduction proving it matters in practice.

Impact

Extremely narrow: requires two tabs open on the very first-ever visit to the origin, with one tab's own page-load time exceeding the other tab's complete install+activate+claim pipeline (a real network round-trip for precaching). Worst case is a single unwanted reload for the second tab on that one shared first visit — not a recurring or update-blocking defect, unlike the original #585 bug this PR fixes.

Suggested direction (not designed here)

A persistent, storage-backed "this origin has activated a service worker before" flag, written by the first tab to observe any real controllerchange, checked synchronously (no Service Worker API await) before falling back to the current live-state heuristic. Needs its own design pass, not a rushed addition to #613.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions