web-pwa-service-workers · git:20260328.03e71dd · 2026-03-28 · sha256 d06d51a2798f91d4
web-pwa-service-workers git:20260328.03e71ddA
Immutable. This exact content is served forever at /api/v1/blob/d06d51a2798f91d4.
---
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, 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 clone responses before caching - `cache.put(request, response.clone())` - response body can only be consumed once)**
**(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
**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:**
- [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
---
<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.
**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.
```
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 from your main application with feature detection, update checking, and user-controlled updates.
```typescript
const SW_PATH = "/sw.js";
const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000;
const registration = await navigator.serviceWorker.register(SW_PATH, {
scope: "/",
updateViaCache: "none", // Always check server for updates
});
// Periodic update checks
setInterval(() => registration.update(), UPDATE_CHECK_INTERVAL_MS);
// 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
}
});
});
```
See [examples/core.md](examples/core.md) Pattern 1 for complete registration with update tracking and reload handling.
---
### Pattern 2: Lifecycle Handlers (Install / Activate / Message)
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) => {
event.waitUntil(
caches.open(CACHES.static).then((cache) => cache.addAll(PRECACHE_URLS)),
);
// Do NOT call skipWaiting here - let user control updates
});
// Activate - cleanup old caches, claim clients
self.addEventListener("activate", (event: ExtendableEvent) => {
event.waitUntil(
caches
.keys()
.then((names) =>
Promise.all(
names
.filter((n) => !currentCaches.includes(n))
.map((n) => caches.delete(n)),
),
)
.then(() => self.clients.claim()),
);
});
// Message - user-controlled skipWaiting
self.addEventListener("message", (event: ExtendableMessageEvent) => {
if (event.data?.type === "SKIP_WAITING") self.skipWaiting();
});
```
See [examples/core.md](examples/core.md) Pattern 2 for complete template with constants and type safety.
---
### Pattern 3: Caching Strategies
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 |
Key implementation details:
- 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
// 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
```
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 4: Fetch Event Routing
Route requests to appropriate caching strategies based on request type and URL.
```typescript
self.addEventListener("fetch", (event: FetchEvent) => {
const { request } = event;
const url = new URL(request.url);
if (request.method !== "GET") return; // Skip non-GET
if (url.origin !== location.origin) return; // Skip cross-origin
if (request.mode === "navigate") {
event.respondWith(networkFirst(request, CACHES.pages));
} else if (request.destination === "image") {
event.respondWith(
cacheFirstWithLimit(request, CACHES.images, MAX_CACHE_ITEMS.images),
);
} else if (url.pathname.startsWith("/api/")) {
event.respondWith(staleWhileRevalidate(request, CACHES.api));
} else {
event.respondWith(cacheFirst(request, CACHES.static));
}
});
```
---
### Pattern 5: Offline Fallback
Always precache an offline.html page and return it when both cache and network fail for navigation requests.
```typescript
// 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;
}
// 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" } },
);
```
---
### Pattern 6: Navigation Preload
Fetch navigation requests in parallel with service worker bootup, reducing latency for network-first HTML. Enable in activate, consume via `event.preloadResponse` in fetch.
```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;
}
```
**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.
See [examples/caching.md](examples/caching.md) Pattern 8 for complete implementation.
---
### Pattern 7: Update Handling
Users should control when updates apply. Detect waiting workers, notify users, and let them trigger `skipWaiting`.
```typescript
// Client: detect and apply updates
if (registration.waiting) {
showUpdateBanner();
}
function applyUpdate() {
registration.waiting?.postMessage({ type: "SKIP_WAITING" });
}
// Reload when new worker takes control
navigator.serviceWorker.addEventListener("controllerchange", () => {
window.location.reload();
});
```
See [examples/updates.md](examples/updates.md) for version tracking, aggressive updates, deferred updates, idle-time updates, progressive rollout, and data migration patterns.
</patterns>
---
<red_flags>
## RED FLAGS
**High Priority Issues:**
- 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
**Medium Priority Issues:**
- 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
**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 clone responses before caching - `cache.put(request, response.clone())` - response body can only be consumed once)**
**(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>