$npx -y skills add jgamaraalv/ts-dev-kit --skill service-workerService Worker API implementation guide — registration, lifecycle management, caching strategies, push notifications, and background sync. Use when: (1) creating or modifying service worker files (sw.js), (2) implementing offline-first caching (cache-first, network-first, stale-w
| 1 | # Service Worker |
| 2 | |
| 3 | ## Table of Contents |
| 4 | |
| 5 | - [Constraints](#constraints) |
| 6 | - [Lifecycle](#lifecycle) |
| 7 | - [Registration](#registration) |
| 8 | - [Install / Activate / Fetch Events](#install-event--pre-cache-assets) |
| 9 | - [Common Pitfalls](#common-pitfalls) |
| 10 | - [Next.js Integration](#nextjs-integration) |
| 11 | - [Reference files](#reference-files) |
| 12 | |
| 13 | <constraints> |
| 14 | |
| 15 | ## Constraints |
| 16 | |
| 17 | - HTTPS required (localhost exempt for dev) |
| 18 | - No DOM access — runs on separate thread |
| 19 | - Fully async — no synchronous XHR, no localStorage |
| 20 | - No dynamic `import()` — only static `import` statements |
| 21 | - Scope defaults to the directory containing the SW file |
| 22 | - `self` refers to `ServiceWorkerGlobalScope` |
| 23 | |
| 24 | </constraints> |
| 25 | |
| 26 | <quick_reference> |
| 27 | |
| 28 | ## Lifecycle |
| 29 | |
| 30 | ``` |
| 31 | register() → Download → Install → [Wait] → Activate → Fetch control |
| 32 | ``` |
| 33 | |
| 34 | 1. **Register** from main thread via `navigator.serviceWorker.register()` |
| 35 | 2. **Install** event fires once — use to pre-cache static assets |
| 36 | 3. **Wait** — new SW waits until all tabs using old SW are closed (skip with `self.skipWaiting()`) |
| 37 | 4. **Activate** event fires — use to clean up old caches |
| 38 | 5. **Fetch** events start flowing — SW controls page network requests |
| 39 | |
| 40 | A document must reload to be controlled (or call `clients.claim()` during activate). |
| 41 | |
| 42 | ## Updating a Service Worker |
| 43 | |
| 44 | - Browser byte-compares the SW file on each navigation (or every 24h) |
| 45 | - New version installs in background while old version still serves |
| 46 | - Increment the cache name (e.g., `v1` → `v2`) in the new version |
| 47 | - Delete old caches in the `activate` handler |
| 48 | - Call `self.skipWaiting()` in `install` to activate immediately |
| 49 | - Call `self.clients.claim()` in `activate` to take control of open pages |
| 50 | |
| 51 | ## DevTools |
| 52 | |
| 53 | - **Chrome**: `chrome://inspect/#service-workers` or Application > Service Workers |
| 54 | - **Firefox**: `about:debugging#/runtime/this-firefox` or Application > Service Workers |
| 55 | - **Edge**: `edge://inspect/#service-workers` or Application > Service Workers |
| 56 | |
| 57 | Unregister, update, and inspect caches from the Application panel. Use "Update on reload" checkbox during development. |
| 58 | |
| 59 | </quick_reference> |
| 60 | |
| 61 | <examples> |
| 62 | |
| 63 | ## Registration |
| 64 | |
| 65 | ```js |
| 66 | // main.js — register from the page |
| 67 | if ("serviceWorker" in navigator) { |
| 68 | const reg = await navigator.serviceWorker.register("/sw.js", { scope: "/" }); |
| 69 | // reg.installing | reg.waiting | reg.active |
| 70 | } |
| 71 | ``` |
| 72 | |
| 73 | **Scope rules:** |
| 74 | |
| 75 | - SW at `/sw.js` can control `/` and all subpaths |
| 76 | - SW at `/app/sw.js` can only control `/app/` by default |
| 77 | - Broaden scope with `Service-Worker-Allowed` response header |
| 78 | |
| 79 | ## Install Event — Pre-cache Assets |
| 80 | |
| 81 | ```js |
| 82 | // sw.js |
| 83 | const CACHE_NAME = "v1"; |
| 84 | const PRECACHE_URLS = ["/", "/index.html", "/style.css", "/app.js"]; |
| 85 | |
| 86 | self.addEventListener("install", (event) => { |
| 87 | event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))); |
| 88 | }); |
| 89 | ``` |
| 90 | |
| 91 | `waitUntil(promise)` — keeps install phase alive until the promise settles. If rejected, installation fails and the SW won't activate. |
| 92 | |
| 93 | ## Activate Event — Clean Up Old Caches |
| 94 | |
| 95 | ```js |
| 96 | self.addEventListener("activate", (event) => { |
| 97 | event.waitUntil( |
| 98 | caches |
| 99 | .keys() |
| 100 | .then((keys) => |
| 101 | Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))), |
| 102 | ), |
| 103 | ); |
| 104 | }); |
| 105 | ``` |
| 106 | |
| 107 | ## Fetch Event — Intercept Requests |
| 108 | |
| 109 | ```js |
| 110 | self.addEventListener("fetch", (event) => { |
| 111 | event.respondWith(caches.match(event.request).then((cached) => cached || fetch(event.request))); |
| 112 | }); |
| 113 | ``` |
| 114 | |
| 115 | `respondWith(promise)` — must be called synchronously (within the event handler, not in a microtask). The promise resolves to a `Response`. |
| 116 | |
| 117 | For caching strategy patterns (cache-first, network-first, stale-while-revalidate), see [references/caching-strategies.md](references/caching-strategies.md). |
| 118 | |
| 119 | ## Navigation Preload |
| 120 | |
| 121 | Avoid the startup delay when a SW boots to handle a navigation: |
| 122 | |
| 123 | ```js |
| 124 | self.addEventListener("activate", (event) => { |
| 125 | event.waitUntil(self.registration?.navigationPreload.enable()); |
| 126 | }); |
| 127 | |
| 128 | self.addEventListener("fetch", (event) => { |
| 129 | event.respondWith( |
| 130 | (async () => { |
| 131 | const cached = await caches.match(event.request); |
| 132 | if (cached) return cached; |
| 133 | |
| 134 | const preloaded = await event.preloadResponse; |
| 135 | if (preloaded) return preloaded; |
| 136 | |
| 137 | return fetch(event.request); |
| 138 | })(), |
| 139 | ); |
| 140 | }); |
| 141 | ``` |
| 142 | |
| 143 | ## Communicating with Pages |
| 144 | |
| 145 | ```js |
| 146 | // Page → SW |
| 147 | navigator.serviceWorker.controller.postMessage({ type: "SKIP_WAI |