Skip to content

fix(pwa): stop precaching the shell and bundles so deploys do not strand open tabs - #8

Merged
balebbae merged 1 commit into
mainfrom
devin/1790307868-sw-stale-shell
Sep 25, 2026
Merged

balebbae merged 1 commit into
mainfrom
devin/1790307868-sw-stale-shell

Conversation

@devin-ai-integration

Copy link
Copy Markdown

Summary

Admins hit Failed to fetch dynamically imported module: /assets/ReviewsPage-<hash>.js (the router's 500 page) after deploys. The chunk 404 itself is inherent to hashed bundles + a fresh Cloud Run image, but the service worker was manufacturing the stale pages that request them:

  • sw.ts used precacheAndRoute over **/*.{js,css,html,...}. Workbox serves / from the precached index.html cache-first, so opening the app (PWA start_url / bookmark) after a deploy loaded the previous build's shell with zero network requests — reproduced locally in Chrome.
  • When the new worker then installed it skipWaiting()s, claims the open tabs, and Workbox's precache cleanup deletes every old chunk. Any tab still running the old build 404s on its next un-visited lazy route.
  • (Outside this repo: Cloudflare returns /sw.js with cache-control: max-age=3600, cf-cache-status: REVALIDATED even though spa.go sends no-cache, so the SW update itself lags up to an hour after a deploy, extending the stale window. Needs a Cache Rule bypassing /sw.js and /registerSW.js.)

Change:

- globPatterns: ["**/*.{js,css,html,png,svg,ico,webp,woff,woff2}"]
+ globPatterns: ["**/*.{png,svg,ico,webp,woff,woff2}"]        // precache = icons/fonts only

  precacheAndRoute(self.__WB_MANIFEST);
+ registerRoute(({ sameOrigin, url }) => sameOrigin && url.pathname.startsWith("/assets/"),
+   new CacheFirst({ cacheName: "harp-assets",
+     plugins: [new ExpirationPlugin({ maxEntries: 600, maxAgeSeconds: 30d, purgeOnQuotaError: true })] }));

Effects, verified with a local two-build "deploy" simulation against Chrome:

  • / is always fetched from the network (no-cache from spa.go), so every open gets the current build's shell.
  • A tab that outlives a deploy keeps its own build's chunks in harp-assets (probed with fetch(..., {cache: "no-store"}) → 200 with no origin request) instead of 404ing; only chunks it never loaded before the deploy still 404, and those are handled by the existing vite:preloadError auto-reload.
  • sw.js no longer changes per deploy (byte-identical across the two builds), so the skipWaiting/claim/cleanup rug-pull stops happening on every merge, and users stop re-downloading a ~7 MB precache each deploy.

Not changed: the transition from the current precaching worker to this one behaves like one last deploy (old precached chunks are cleaned up once); push handlers are untouched.

Link to Devin session: https://app.devin.ai/sessions/00c881e94b064b5780794038f0f4813e
Open in Devin Desktop: https://app.devin.ai/desktop/session/00c881e94b064b5780794038f0f4813e?variant=devin
Requested by: @balebbae

…and open tabs

The service worker precached index.html and every hashed chunk. Opening the
app at / was served the previous build's shell from the precache, and when
the new worker activated it deleted the old chunks, so those stale tabs 404'd
on the next lazy route. The shell now always comes from the network and
/assets/* is runtime-cached (CacheFirst, 30d) so a tab keeps its own build's
chunks across deploys; sw.js no longer changes per deploy.
@devin-ai-integration

Copy link
Copy Markdown
Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@balebbae
balebbae merged commit 168dcf1 into main Sep 25, 2026
3 checks passed
@balebbae
balebbae deleted the devin/1790307868-sw-stale-shell branch September 25, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant