pwa-offline-cache-contracts

Installation
SKILL.md

PWA offline cache contracts

A service worker turns your app into cached bytes that outlive any single deploy, so every cache decision is a freshness contract: what is precached, when a new worker takes over, when old caches are deleted, and what must never be cached at all. The bugs are not in the offline happy path — they are stale builds that never update, navigations that fail on purged chunks, and per-user responses served to the wrong session.

Checklist (lead with the trap)

  1. A long-cached index.html plus content-hashed chunks is the classic post-deploy break. The HTML is cached with a long max-age while a deploy emits new hashed chunks (app-B2.js) and purges the old (app-A1.js). Returning users load the stale HTML, it still asks for the purged chunk, and the dynamic import rejects with ChunkLoadError (webpack) or Failed to fetch dynamically imported module (Vite native ESM). Serve HTML with Cache-Control: no-cache (revalidate every load) or route it Network First in the SW, keep hashed assets immutable/Cache First, and add a vite:preloadError / lazy-import handler that reloads once with a retry cap — never an unbounded reload loop.
  2. The waiting worker is why "I deployed but users still see the old app." A new worker installs then sits in waiting until every client controlled by the old worker closes; a refresh does not release it because clients overlap. So a deploy silently does nothing for active tabs. Detect the update (updatefound -> statechange to installed, and also check registration.waiting on load in case the prompt was missed) and give the user a path to activate. registration.update() only refetches the script; a byte-different worker still waits.
  3. Delete old caches in activate, keyed by an explicit version — not in install. Open a versioned cache name per release (app-static-v3); on activate, walk caches.keys() and delete any not in your current allowlist. Do cleanup in activate, not install, because the old worker is still serving pages while the new one installs. Installing a new worker does not evict old caches for you; without cleanup they accumulate against the storage quota.
  4. cache.addAll is all-or-nothing: one bad URL aborts the whole install. cache.addAll is atomic (Workbox precaching fetches entries individually rather than via addAll; a failed entry likewise rejects install so the new worker never activates, though entries already written are not rolled back) — if any single request fails (a 404, a redirect, an opaque cross-origin response), the promise rejects, and wrapped in event.waitUntil the install fails, so nothing is cached and the worker never activates. These rejections are hard to see from inside the worker. Precache only same-origin URLs you expect to return 200; cache optional or third-party assets separately with cache.put and per-item error handling.
  5. Custom globPatterns replace the default match set — enumerate every asset type. In workbox-build / vite-plugin-pwa, setting globPatterns overrides the defaults instead of extending them. The defaults cover only a subset (JS/CSS/HTML); once you set your own, woff2 fonts, png/svg icons, and JSON drop out of the precache and the app breaks only offline. List every extension the app needs and mind maximumFileSizeToCacheInBytes (large files are silently skipped). A built-manifest assertion (HTML + JS + CSS + icons + fonts all present) catches the regression.
  6. Decide skipWaiting / clients.claim vs prompt-to-reload deliberately. self.skipWaiting() activates the new worker immediately, so it can control pages loaded by the old version — early fetches were served old, later ones new, mixing mismatched HTML and chunks. clients.claim() lets an active worker control already-open tabs. Both are fine for pure precache-and-serve; for an app where mixed versions break, prefer a prompt: skip waiting only when the user accepts, then reload on controllerchange. Do not paste skipWaiting + clients.claim as boilerplate without deciding.
  7. Give navigations a fallback and a real offline page. For an SPA, register a NavigationRoute serving the precached shell (createHandlerBoundToURL('/index.html')) so client-side routes resolve offline, with allowlist/denylist so it does not swallow API or file URLs (it matches all navigations by default). Provide a dedicated offline fallback for navigations that miss the cache — otherwise a first offline visit shows the browser error page, not your app.
  8. Never precache or runtime-cache authenticated HTML/API responses. Cache Storage is per-origin, not per-user, and the Cache API ignores HTTP cache headers — whatever you put stays until you delete it. Caching an authenticated page or per-user API JSON lets the next session (logout/login, shared device) read another user's data, or stale-after-logout data. Route authenticated/personalized endpoints Network Only (never written to cache); reserve Cache First for hash-versioned static assets and Network First for shell HTML, and gate cacheable responses to status 200 so opaque (status 0) responses are not stored blindly.

Quick probes

Treat hits as leads; open the SW source and the build config before filing. Route mechanical checks to your build tool's Workbox output (glob count, precache size, size-limit warnings) and DevTools Application panel — Service Workers shows the waiting worker and the Update-on-reload switch, Cache Storage shows what was actually precached. (Lighthouse's PWA category was removed in v12; do not point CI at it.)

Installs
1
GitHub Stars
1
First Seen
3 days ago
pwa-offline-cache-contracts — voidmatcha/frontend-niche-skills