git:20260202.b236384 to git:20260316.00cb75b

152 added, 450 removed. Audit A to A.

---
name: web-pwa-service-workers
description: Service Worker lifecycle, caching strategies, offline patterns, update handling, precaching, runtime caching
---
# Service Worker Patterns
- > **Quick Guide:** Use Service Workers for offline-first applications with sophisticated caching. Implement cache-first for static assets, network-first for HTML, and stale-while-revalidate for API data. Always handle the install/activate/fetch lifecycle properly and provide user control over updates.
+ > **Quick Guide:** Use Service Workers for offline-first applications with sophisticated caching. Implement cache-first for static assets, network-first for HTML, and stale-while-revalidate for API data. Always handle the install/activate/fetch lifecycle properly, version your caches, and provide user control over updates. Clone responses before caching (body can only be consumed once).
---
<critical_requirements>
## CRITICAL: Before Using This Skill
> **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
**(You MUST call `event.waitUntil()` in install and activate handlers to signal completion)**
**(You MUST version your caches and clean up old versions during activation)**
- **(You MUST implement proper update detection and give users control over when updates apply)**
+ **(You MUST clone responses before caching - `cache.put(request, response.clone())` - response body can only be consumed once)**
- **(You MUST use `clients.claim()` in activate if you need immediate control of clients)**
+ **(You MUST implement proper update detection and give users control over when updates apply)**
**(You MUST handle all fetch failures with appropriate offline fallbacks)**
</critical_requirements>
---
**Auto-detection:** Service Worker, serviceWorker, sw.js, sw.ts, navigator.serviceWorker, caches, Cache API, CacheStorage, skipWaiting, clients.claim, precache, offline-first, PWA
**When to use:**
- Building Progressive Web Apps (PWAs) with offline support
- Implementing sophisticated caching strategies beyond browser defaults
- Providing offline fallback pages or cached content
- Controlling how network requests are handled and cached
- **Key patterns covered:**
-
- - Service Worker lifecycle (install, activate, fetch)
- - Caching strategies (cache-first, network-first, stale-while-revalidate)
- - Precaching critical assets during installation
- - Runtime caching for dynamic content
- - Update detection and user-controlled updates
- - Offline fallback pages
-
**When NOT to use:**
- Simple websites without offline requirements
- When browser HTTP caching is sufficient
- For real-time data that must always be fresh (use network-only)
- **Detailed Resources:**
-
- - For code examples, see [examples/](examples/)
- - For decision frameworks and anti-patterns, see [reference.md](reference.md)
-
---
<philosophy>
## Philosophy
- Service Workers are **programmable network proxies** that run in a separate thread, intercepting requests between your application and the network. They enable offline functionality, sophisticated caching, and background operations that weren't previously possible on the web.
+ Service Workers are **programmable network proxies** that run in a separate thread, intercepting requests between your application and the network. They enable offline functionality, sophisticated caching, and background operations.
**The Service Worker lifecycle is designed for safety:**
1. **Install Phase:** Download and cache critical assets. The worker is "waiting" until installation completes.
-
2. **Waiting Phase:** New workers wait for all tabs using the old worker to close, preventing version conflicts.
-
3. **Activate Phase:** Old caches are cleaned up, and the worker takes control.
-
4. **Fetch Phase:** The active worker intercepts all network requests within its scope.
- **Core Principles:**
-
- 1. **Safety First:** The lifecycle exists to prevent running multiple versions simultaneously, which could corrupt state or cause inconsistent behavior.
-
- 2. **User Control:** Users should decide when updates apply, not be surprised by sudden behavior changes mid-session.
-
- 3. **Graceful Degradation:** Always provide fallbacks when network and cache both fail.
-
- 4. **Cache Versioning:** Version your caches to enable clean upgrades and prevent unbounded growth.
-
- **Lifecycle Diagram:**
-
```
Registration → Download → Install → Waiting → Activate → Fetch
↓ ↓
(skipWaiting) (claim)
```
+ **Core Principles:**
+
+ 1. **Safety First:** The lifecycle prevents running multiple versions simultaneously, which could corrupt state.
+ 2. **User Control:** Users should decide when updates apply, not be surprised by sudden behavior changes mid-session.
+ 3. **Graceful Degradation:** Always provide fallbacks when network and cache both fail.
+ 4. **Cache Versioning:** Version your caches to enable clean upgrades and prevent unbounded growth.
+
</philosophy>
---
<patterns>
## Core Patterns
### Pattern 1: Service Worker Registration
- Register the service worker from your main application with proper error handling and update detection.
+ Register from your main application with feature detection, update checking, and user-controlled updates.
```typescript
- // register-service-worker.ts
const SW_PATH = "/sw.js";
- const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
-
- interface ServiceWorkerState {
- registration: ServiceWorkerRegistration | null;
- updateAvailable: boolean;
- applyUpdate: () => void;
- }
-
- async function registerServiceWorker(): Promise<ServiceWorkerState> {
- if (!("serviceWorker" in navigator)) {
- console.log("Service Workers not supported");
- return {
- registration: null,
- updateAvailable: false,
- applyUpdate: () => {},
- };
- }
-
- try {
- const registration = await navigator.serviceWorker.register(SW_PATH, {
- scope: "/",
- updateViaCache: "none", // Always check server for updates
- });
-
- console.log("Service Worker registered:", registration.scope);
-
- // Check for updates periodically
- setInterval(() => {
- registration.update();
- }, UPDATE_CHECK_INTERVAL_MS);
+ const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000;
- // Track update availability
- let updateAvailable = false;
- let waitingWorker: ServiceWorker | null = null;
+ const registration = await navigator.serviceWorker.register(SW_PATH, {
+ scope: "/",
+ updateViaCache: "none", // Always check server for updates
+ });
- const handleUpdate = (worker: ServiceWorker) => {
- waitingWorker = worker;
- updateAvailable = true;
- // Notify UI that update is available
- window.dispatchEvent(new CustomEvent("sw-update-available"));
- };
+ // Periodic update checks
+ setInterval(() => registration.update(), UPDATE_CHECK_INTERVAL_MS);
- // Check if already waiting
- if (registration.waiting) {
- handleUpdate(registration.waiting);
+ // Track waiting worker for user-controlled updates
+ registration.addEventListener("updatefound", () => {
+ const installing = registration.installing;
+ installing?.addEventListener("statechange", () => {
+ if (
+ installing.state === "installed" &&
+ navigator.serviceWorker.controller
+ ) {
+ // New version waiting - notify user
}
-
- // Listen for new installations
- registration.addEventListener("updatefound", () => {
- const installing = registration.installing;
- if (!installing) return;
-
- installing.addEventListener("statechange", () => {
- if (
- installing.state === "installed" &&
- navigator.serviceWorker.controller
- ) {
- handleUpdate(installing);
- }
- });
- });
-
- // Reload when new worker takes control
- let refreshing = false;
- navigator.serviceWorker.addEventListener("controllerchange", () => {
- if (!refreshing) {
- refreshing = true;
- window.location.reload();
- }
- });
-
- return {
- registration,
- updateAvailable,
- applyUpdate: () => {
- if (waitingWorker) {
- waitingWorker.postMessage({ type: "SKIP_WAITING" });
- }
- },
- };
- } catch (error) {
- console.error("Service Worker registration failed:", error);
- return {
- registration: null,
- updateAvailable: false,
- applyUpdate: () => {},
- };
- }
- }
-
- export { registerServiceWorker };
- export type { ServiceWorkerState };
+ });
+ });
```
- **Why good:** Named constant for interval, proper feature detection, periodic update checks, tracks waiting worker for user-controlled updates, handles controller change to reload, named exports
+ See [examples/core.md](examples/core.md) Pattern 1 for complete registration with update tracking and reload handling.
---
- ### Pattern 2: Basic Service Worker Structure
-
- The service worker file with proper lifecycle handling, cache versioning, and type safety.
-
- #### Constants
-
- ```typescript
- // sw.ts
- declare const self: ServiceWorkerGlobalScope;
-
- const CACHE_VERSION = "v1.0.0";
-
- const CACHES = {
- static: `static-${CACHE_VERSION}`,
- pages: `pages-${CACHE_VERSION}`,
- images: `images-${CACHE_VERSION}`,
- api: `api-${CACHE_VERSION}`,
- } as const;
-
- const PRECACHE_URLS = [
- "/",
- "/offline.html",
- "/manifest.json",
- "/styles/app.css",
- "/scripts/app.js",
- ] as const;
-
- const MAX_CACHE_ITEMS = {
- images: 100,
- api: 50,
- } as const;
-
- const NETWORK_TIMEOUT_MS = 3000;
- ```
+ ### Pattern 2: Lifecycle Handlers (Install / Activate / Message)
- #### Install Handler
+ The three essential lifecycle event handlers: precache in install, cleanup in activate, user-controlled skipWaiting via message.
```typescript
// Install - precache critical assets
self.addEventListener("install", (event: ExtendableEvent) => {
- console.log("[SW] Installing version:", CACHE_VERSION);
-
event.waitUntil(
- (async () => {
- const cache = await caches.open(CACHES.static);
- await cache.addAll(PRECACHE_URLS);
- console.log("[SW] Precached", PRECACHE_URLS.length, "assets");
- // Do NOT call skipWaiting here - let user control updates
- })(),
+ caches.open(CACHES.static).then((cache) => cache.addAll(PRECACHE_URLS)),
);
+ // Do NOT call skipWaiting here - let user control updates
});
- ```
- #### Activate Handler
-
- ```typescript
- // Activate - cleanup old caches
+ // Activate - cleanup old caches, claim clients
self.addEventListener("activate", (event: ExtendableEvent) => {
- console.log("[SW] Activating version:", CACHE_VERSION);
-
event.waitUntil(
- (async () => {
- // Delete old caches
- const cacheNames = await caches.keys();
- const currentCaches = Object.values(CACHES);
-
- await Promise.all(
- cacheNames
- .filter((name) => !currentCaches.includes(name))
- .map((name) => {
- console.log("[SW] Deleting old cache:", name);
- return caches.delete(name);
- }),
- );
-
- // Take control of all clients immediately
- await self.clients.claim();
- console.log("[SW] Claimed all clients");
- })(),
+ caches
+ .keys()
+ .then((names) =>
+ Promise.all(
+ names
+ .filter((n) => !currentCaches.includes(n))
+ .map((n) => caches.delete(n)),
+ ),
+ )
+ .then(() => self.clients.claim()),
);
});
- ```
- #### Message Handler
-
- ```typescript
- // Handle messages from clients
+ // Message - user-controlled skipWaiting
self.addEventListener("message", (event: ExtendableMessageEvent) => {
- if (event.data?.type === "SKIP_WAITING") {
- console.log("[SW] Skip waiting requested");
- self.skipWaiting();
- }
+ if (event.data?.type === "SKIP_WAITING") self.skipWaiting();
});
```
- **Why good:** All constants named and typed, cache versioning for clean upgrades, waitUntil signals completion, old caches cleaned up, message handler for user-controlled skipWaiting
-
- ---
-
- ### Pattern 3: Cache-First Strategy
-
- Return cached response immediately, fall back to network. Best for static assets that don't change often.
-
- ```typescript
- // Cache-first: Check cache, fall back to network
- async function cacheFirst(
- request: Request,
- cacheName: string,
- ): Promise<Response> {
- const cache = await caches.open(cacheName);
- const cachedResponse = await cache.match(request);
-
- if (cachedResponse) {
- console.log("[SW] Cache hit:", request.url);
- return cachedResponse;
- }
-
- console.log("[SW] Cache miss, fetching:", request.url);
-
- try {
- const networkResponse = await fetch(request);
-
- // Only cache successful responses
- if (networkResponse.ok) {
- // Clone before caching (response can only be consumed once)
- cache.put(request, networkResponse.clone());
- }
-
- return networkResponse;
- } catch (error) {
- console.error("[SW] Fetch failed:", request.url, error);
- // Return offline fallback for navigation requests
- if (request.mode === "navigate") {
- const offlinePage = await caches.match("/offline.html");
- if (offlinePage) return offlinePage;
- }
- throw error;
- }
- }
- ```
-
- **When to use:** Static assets (CSS, JS, images), fonts, versioned resources with cache-busting hashes
-
- **Why good:** Checks cache first for speed, caches network responses, clones before put, handles failures with offline fallback
+ See [examples/core.md](examples/core.md) Pattern 2 for complete template with constants and type safety.
---
- ### Pattern 4: Network-First Strategy
-
- Try network first, fall back to cache if offline. Best for content that should be fresh but work offline.
-
- ```typescript
- // Network-first: Try network, fall back to cache
- async function networkFirst(
- request: Request,
- cacheName: string,
- timeoutMs: number = NETWORK_TIMEOUT_MS,
- ): Promise<Response> {
- const cache = await caches.open(cacheName);
-
- try {
- // Race network request against timeout
- const networkResponse = await Promise.race([
- fetch(request),
- new Promise<never>((_, reject) =>
- setTimeout(() => reject(new Error("Network timeout")), timeoutMs),
- ),
- ]);
-
- // Cache successful responses
- if (networkResponse.ok) {
- cache.put(request, networkResponse.clone());
- }
-
- return networkResponse;
- } catch (error) {
- console.log("[SW] Network failed, trying cache:", request.url);
-
- const cachedResponse = await cache.match(request);
- if (cachedResponse) {
- return cachedResponse;
- }
-
- // Return offline page for navigation requests
- if (request.mode === "navigate") {
- const offlinePage = await caches.match("/offline.html");
- if (offlinePage) return offlinePage;
- }
-
- throw error;
- }
- }
- ```
-
- **When to use:** HTML pages, API requests that need fresh data but should work offline, frequently updated content
+ ### Pattern 3: Caching Strategies
- **Why good:** Timeout prevents hanging on slow connections, caches successful responses for offline, proper fallback chain
+ Four strategies to match content types:
- ---
+ | Strategy | When to Use | Behavior |
+ | ----------------------------- | ---------------------------------- | ------------------------------------------- |
+ | **Cache-first** | Static assets, fonts, hashed files | Return cached immediately, network fallback |
+ | **Network-first** | HTML pages, user-specific API data | Try network with timeout, cache fallback |
+ | **Stale-while-revalidate** | Avatars, non-critical API, feeds | Return cached, refresh in background |
+ | **Cache-only / Network-only** | Precached shells / real-time data | Single source, no fallback |
- ### Pattern 5: Stale-While-Revalidate Strategy
+ Key implementation details:
- Return cached response immediately, update cache in background. Best for content where speed matters but staleness is acceptable.
+ - Always check `response.ok` before caching (avoid caching 404/500)
+ - Always `response.clone()` before `cache.put()` (body consumed once)
+ - Add timeout to network-first to avoid hanging on slow connections
+ - Limit cache size to prevent unbounded storage growth
```typescript
- // Stale-while-revalidate: Return cache, update in background
- async function staleWhileRevalidate(
- request: Request,
- cacheName: string,
- ): Promise<Response> {
- const cache = await caches.open(cacheName);
- const cachedResponse = await cache.match(request);
-
- // Background revalidation (don't await)
- const fetchPromise = fetch(request)
- .then(async (networkResponse) => {
- if (networkResponse.ok) {
- await cache.put(request, networkResponse.clone());
- console.log("[SW] Cache updated:", request.url);
- }
- return networkResponse;
- })
- .catch((error) => {
- console.error("[SW] Background fetch failed:", request.url, error);
- return null;
- });
-
- // Return cached immediately if available, otherwise wait for network
- if (cachedResponse) {
- console.log("[SW] Returning stale:", request.url);
- return cachedResponse;
- }
-
- console.log("[SW] No cache, waiting for network:", request.url);
- const networkResponse = await fetchPromise;
- if (networkResponse) {
- return networkResponse;
- }
-
- throw new Error("No cached or network response available");
+ // The clone pattern - response body can only be consumed once
+ const networkResponse = await fetch(request);
+ if (networkResponse.ok) {
+ cache.put(request, networkResponse.clone()); // Clone for cache
}
+ return networkResponse; // Original for client
```
- **When to use:** User avatars, non-critical API data, content that should be fast but eventually consistent
-
- **Why good:** Returns immediately for cached content, background fetch doesn't block, handles both cache-hit and cache-miss scenarios
+ See [examples/core.md](examples/core.md) Pattern 2 for all strategy implementations in the complete template, and [examples/caching.md](examples/caching.md) for advanced patterns (expiration, selective API caching, storage cleanup).
---
- ### Pattern 6: Fetch Event Router
+ ### Pattern 4: Fetch Event Routing
Route requests to appropriate caching strategies based on request type and URL.
```typescript
- // Fetch event handler - routes to strategies
self.addEventListener("fetch", (event: FetchEvent) => {
const { request } = event;
const url = new URL(request.url);
- // Skip non-GET requests
- if (request.method !== "GET") {
- return;
- }
-
- // Skip cross-origin requests
- if (url.origin !== location.origin) {
- return;
- }
+ if (request.method !== "GET") return; // Skip non-GET
+ if (url.origin !== location.origin) return; // Skip cross-origin
- // Route based on request type
if (request.mode === "navigate") {
- // HTML pages: Network-first
event.respondWith(networkFirst(request, CACHES.pages));
} else if (request.destination === "image") {
- // Images: Cache-first with size limit
event.respondWith(
cacheFirstWithLimit(request, CACHES.images, MAX_CACHE_ITEMS.images),
);
} else if (url.pathname.startsWith("/api/")) {
- // API requests: Stale-while-revalidate
event.respondWith(staleWhileRevalidate(request, CACHES.api));
} else {
- // Static assets: Cache-first
event.respondWith(cacheFirst(request, CACHES.static));
}
});
```
- **Why good:** Clear routing logic, skips non-cacheable requests, uses appropriate strategy per content type, named constants for cache names
-
---
- ### Pattern 7: Cache Size Management
+ ### Pattern 5: Offline Fallback
- Limit cache size to prevent unbounded storage growth.
+ Always precache an offline.html page and return it when both cache and network fail for navigation requests.
```typescript
- // Limit cache size by removing oldest entries
- async function limitCacheSize(cache: Cache, maxItems: number): Promise<void> {
- const keys = await cache.keys();
-
- if (keys.length > maxItems) {
- // Delete oldest entries (FIFO)
- const deleteCount = keys.length - maxItems;
- const toDelete = keys.slice(0, deleteCount);
-
- await Promise.all(
- toDelete.map((request) => {
- console.log("[SW] Evicting from cache:", request.url);
- return cache.delete(request);
- }),
- );
- }
+ // In install handler: precache offline.html
+ // In fetch error handling:
+ if (request.mode === "navigate") {
+ const offlinePage = await caches.match("/offline.html");
+ if (offlinePage) return offlinePage;
}
- // Cache-first with size limit
- async function cacheFirstWithLimit(
- request: Request,
- cacheName: string,
- maxItems: number,
- ): Promise<Response> {
- const cache = await caches.open(cacheName);
- const cachedResponse = await cache.match(request);
+ // Last resort: inline response
+ return new Response(
+ "<html><body><h1>Offline</h1><p>Check your connection.</p></body></html>",
+ { status: 503, headers: { "Content-Type": "text/html" } },
+ );
+ ```
- if (cachedResponse) {
- return cachedResponse;
- }
+ ---
- try {
- const networkResponse = await fetch(request);
+ ### Pattern 6: Navigation Preload
- if (networkResponse.ok) {
- // Limit cache size before adding
- await limitCacheSize(cache, maxItems - 1);
- cache.put(request, networkResponse.clone());
- }
+ Fetch navigation requests in parallel with service worker bootup, reducing latency for network-first HTML. Enable in activate, consume via `event.preloadResponse` in fetch.
- return networkResponse;
- } catch (error) {
- console.error("[SW] Fetch failed:", request.url);
- throw error;
- }
+ ```typescript
+ // Activate: enable navigation preload
+ if (self.registration.navigationPreload) {
+ await self.registration.navigationPreload.enable();
}
+
+ // Fetch: use preloaded response (avoids double fetch)
+ const preloadResponse = await event.preloadResponse;
+ if (preloadResponse) {
+ cache.put(event.request, preloadResponse.clone());
+ return preloadResponse;
+ }
```
- **Why good:** Prevents storage quota issues, FIFO eviction is predictable, limits checked before adding new entries
+ **Warning:** If you enable navigation preload, you MUST use `event.preloadResponse`. Using `fetch(event.request)` instead results in two network requests for the same resource.
- ---
+ **When to use:** Network-first HTML pages with dynamic/authenticated content. Not needed for precached app shells.
- ### Pattern 8: Offline Fallback Page
+ See [examples/caching.md](examples/caching.md) Pattern 8 for complete implementation.
- Create a meaningful offline experience when both cache and network fail.
+ ---
- ```typescript
- // offline.html should be precached during install
- // Return it for navigation requests that fail
+ ### Pattern 7: Update Handling
- async function handleNavigationFailure(request: Request): Promise<Response> {
- // Try the cache first
- const cachedPage = await caches.match(request);
- if (cachedPage) {
- return cachedPage;
- }
+ Users should control when updates apply. Detect waiting workers, notify users, and let them trigger `skipWaiting`.
- // Return offline page
- const offlinePage = await caches.match("/offline.html");
- if (offlinePage) {
- return offlinePage;
- }
+ ```typescript
+ // Client: detect and apply updates
+ if (registration.waiting) {
+ showUpdateBanner();
+ }
- // Last resort: return a basic offline response
- return new Response(
- `<!DOCTYPE html>
- <html>
- <head><title>Offline</title></head>
- <body>
- <h1>You are offline</h1>
- <p>Please check your internet connection and try again.</p>
- </body>
- </html>`,
- {
- status: 503,
- headers: { "Content-Type": "text/html" },
- },
- );
+ function applyUpdate() {
+ registration.waiting?.postMessage({ type: "SKIP_WAITING" });
}
+
+ // Reload when new worker takes control
+ navigator.serviceWorker.addEventListener("controllerchange", () => {
+ window.location.reload();
+ });
```
- **Why good:** Graceful degradation chain, always returns something meaningful, inline fallback as last resort
+ See [examples/updates.md](examples/updates.md) for version tracking, aggressive updates, deferred updates, idle-time updates, progressive rollout, and data migration patterns.
</patterns>
---
- <integration>
+ **Detailed Resources:**
- ## Integration Guide
+ - [examples/core.md](examples/core.md) - Registration, lifecycle template, caching strategy implementations, types
+ - [examples/caching.md](examples/caching.md) - Advanced caching (expiration, selective API, storage cleanup, navigation preload)
+ - [examples/updates.md](examples/updates.md) - Version tracking, update strategies (aggressive, deferred, idle, rollout, migration)
+ - [reference.md](reference.md) - Decision frameworks, anti-patterns, lifecycle reference, checklists
- **Service Workers are framework-agnostic.** This skill covers the native Service Worker API only. Integration with specific build tools or libraries is handled by their respective documentation.
+ ---
- **Works with:**
+ <red_flags>
- - Your build tool for precache manifest generation
- - Your web app manifest for PWA installability
- - Your offline page design for user experience
+ ## RED FLAGS
- **Defers to:**
+ **High Priority Issues:**
- - Background Sync API (for queuing failed requests)
- - Push API (for push notifications)
- - IndexedDB (for offline data storage beyond cache)
- - Navigation Preload API (for parallel navigation fetching during SW bootup)
+ - No `event.waitUntil()` in install/activate - browser may terminate SW before async operations complete
+ - Calling `skipWaiting()` unconditionally in install - users experience unexpected behavior changes mid-session
+ - No cache versioning - old cached content persists forever, storage grows unbounded
+ - Not cleaning up old caches in activate - storage quota eventually exceeded
+ - Missing offline fallback - users see browser error page instead of helpful message
+ - Not checking `response.ok` before caching - error responses (404, 500) get cached and served
- **Recommended Tools:**
+ **Medium Priority Issues:**
- - **Workbox** - Google's production-ready library for service workers. Provides pre-built caching strategies, precaching with revision management, background sync plugins, and navigation preload support. Use Workbox for production applications instead of hand-rolling all patterns.
+ - No timeout on network requests in network-first strategy - fetch hangs indefinitely on slow connections
+ - Not cloning response before caching - response body consumed, client gets empty response
+ - No cache size limits - unbounded growth leads to quota issues
+ - Attempting to cache POST requests - only GET requests are cacheable
- </integration>
+ **Gotchas & Edge Cases:**
+ - Service workers only work over HTTPS (exception: localhost for development)
+ - Scope determined by SW file location - `/sw.js` controls `/`, but `/scripts/sw.js` only controls `/scripts/`
+ - Browser may terminate idle service workers - do not rely on in-memory state
+ - `clients.claim()` does not trigger reload - clients keep running old page with new SW
+ - Chrome DevTools "Update on reload" bypasses waiting - useful for dev, not representative of production
+ - Web app manifest changes do not trigger SW update - only byte changes to SW file itself
+ - IndexedDB transactions cannot span `await` - complete DB work in single transaction
+ - Opaque responses (cross-origin without CORS) count against storage quota at inflated cost
+ - Service worker bootup varies: ~50ms desktop, ~250ms mobile, 500ms+ slow devices - navigation preload mitigates this
+
+ </red_flags>
+
---
<critical_reminders>
## CRITICAL REMINDERS
> **All code must follow project conventions in CLAUDE.md**
**(You MUST call `event.waitUntil()` in install and activate handlers to signal completion)**
**(You MUST version your caches and clean up old versions during activation)**
- **(You MUST implement proper update detection and give users control over when updates apply)**
+ **(You MUST clone responses before caching - `cache.put(request, response.clone())` - response body can only be consumed once)**
- **(You MUST use `clients.claim()` in activate if you need immediate control of clients)**
+ **(You MUST implement proper update detection and give users control over when updates apply)**
**(You MUST handle all fetch failures with appropriate offline fallbacks)**
**Failure to follow these rules will result in broken updates, unbounded cache growth, and poor offline experience.**
</critical_reminders>