From f84d934da5a2378031f70c3e9befd95cf22b2fa7 Mon Sep 17 00:00:00 2001 From: jay Date: Fri, 12 Jun 2026 07:52:34 -0400 Subject: [PATCH] =?UTF-8?q?SW:=20non-disruptive=20update=20=E2=80=94=20dro?= =?UTF-8?q?p=20skipWaiting/claim=20(post-deploy=20boot=20stall)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Telemetry isolated the last boot-slow source (id9): warm edge, fast shell (60ms), but chunks took 2.6-4.7s on the LAN-fast dev box, 26min post-deploy — i.e. NOT network/origin. Cause: on the first load of a new build the new SW ran skipWaiting()+clients.claim(), activating mid-boot — deleting the old cache and seizing the loading page, yanking the cache from under the ~16 in-flight chunk requests (the sz0 + clustered-start + staggered-finish signature). Fix (Codex-approved): remove skipWaiting() and clients.claim() so a new worker installs quietly and takes control on the NEXT navigation, never mid-boot. The post-deploy first load then completes under the stable old worker (cache intact) against the warmed edge. Cache cleanup stays in activate (now runs only at the deferred, safe activation); old immutable chunks live 14 days at the origin regardless, so a slightly-behind worker still loads safely. Trade-off — SW/shell update applies one navigation later — is fine: the shell is edge-cached and the SW's only job is offline/slow-network fallback. Co-Authored-By: Claude Opus 4.8 --- frontend/src/service-worker.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/service-worker.js b/frontend/src/service-worker.js index 1d3c15c..0f233e7 100644 --- a/frontend/src/service-worker.js +++ b/frontend/src/service-worker.js @@ -40,17 +40,23 @@ function isMutablePath(p) { self.addEventListener('install', (event) => { // Best-effort: grab the app shell as an offline fallback. No bulk precache. - event.waitUntil( - caches.open(CACHE).then((c) => c.add('/').catch(() => {})).then(() => self.skipWaiting()) - ); + // Deliberately NO skipWaiting(): a new worker installs quietly and waits, so + // it never seizes a page that's mid-boot. It takes control on the next + // navigation, when the old worker has no clients — the post-deploy first-load + // then completes under the stable old worker (with its cache intact) against + // the warmed edge, instead of having its cache yanked mid-load. + event.waitUntil(caches.open(CACHE).then((c) => c.add('/').catch(() => {}))); }); self.addEventListener('activate', (event) => { + // Runs only when this worker actually activates (next navigation, never + // mid-boot), so deleting old version caches here is safe — and old immutable + // chunks live on at the origin for a 14-day grace window regardless. No + // clients.claim(): pages adopt this worker on their own next navigation. event.waitUntil( caches .keys() .then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))) - .then(() => self.clients.claim()) ); });