service-worker-offline-first
Provides guidance on Service Worker lifecycles and caching strategies for offline-first web apps.
Install
mkdir -p .claude/skills/service-worker-offline-first && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/10544" && unzip -o skill.zip -d .claude/skills/service-worker-offline-first && rm skill.zipInstalls to .claude/skills/service-worker-offline-first
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
>-Key capabilities
- →Implement Cache-First strategy
- →Configure Network-First with timeout
- →Manage Service Worker lifecycle
- →Handle background sync
How it works
Routes fetch events to specific caching strategies based on request type and network availability. It uses lifecycle events to manage cache cleanup and immediate activation.
Inputs & outputs
When to use service-worker-offline-first
- →Implementing offline page fallbacks
- →Configuring cache-first strategies for assets
- →Handling dynamic API content with network-first
- →Managing service worker installation lifecycle
About this skill
Service Worker Offline-First Patterns
Resilient web applications that work when the network doesn't.
Caching Strategy Selection
| Strategy | Latency | Freshness | Best For |
|---|---|---|---|
| Cache-First | Fastest | Stale risk | Static assets, fonts, icons |
| Network-First | Slower | Always fresh | API calls, dynamic content |
| Stale-While-Revalidate | Fast | Eventually fresh | Semi-static content, user profiles |
| Network-Only | Network-dependent | Always fresh | Authentication, real-time data |
| Cache-Only | Fastest | Frozen | App shell, offline page |
Service Worker Lifecycle
Install → waitUntil(cacheStaticAssets) → skipWaiting()
↓
Activate → waitUntil(cleanOldCaches) → clients.claim()
↓
Fetch → route to strategy by request type
Core Structure
const CACHE_VERSION = 'v1';
const STATIC_CACHE = `app-static-${CACHE_VERSION}`;
const DYNAMIC_CACHE = `app-dynamic-${CACHE_VERSION}`;
const API_CACHE = `app-api-${CACHE_VERSION}`;
const STATIC_ASSETS = [
'/',
'/index.html',
'/offline.html',
'/manifest.json',
];
// Install - Cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => cache.addAll(STATIC_ASSETS))
);
self.skipWaiting();
});
// Activate - Clean up old caches
self.addEventListener('activate', (event) => {
const currentCaches = [STATIC_CACHE, DYNAMIC_CACHE, API_CACHE];
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys.filter((key) => !currentCaches.includes(key))
.map((key) => caches.delete(key))
)
)
);
self.clients.claim();
});
// Fetch - Route to appropriate strategy
self.addEventListener('fetch', (event) => {
const { request } = event;
if (request.method !== 'GET') return;
const url = new URL(request.url);
if (!url.protocol.startsWith('http')) return;
if (isApiRequest(request)) {
event.respondWith(networkFirstWithTimeout(request, API_CACHE, 5000));
} else if (request.destination === 'image') {
event.respondWith(cacheFirst(request, STATIC_CACHE));
} else if (request.mode === 'navigate') {
event.respondWith(networkFirstWithTimeout(request, DYNAMIC_CACHE, 3000));
} else {
event.respondWith(staleWhileRevalidate(request, DYNAMIC_CACHE));
}
});
Caching Strategies
Cache-First with Expiry
async function cacheFirstWithExpiry(request, cacheName, maxAge) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
if (cached) {
const cachedDate = new Date(cached.headers.get('sw-cached-date') || 0);
if (Date.now() - cachedDate.getTime() < maxAge) {
return cached; // Fresh cache hit
}
}
try {
const response = await fetch(request);
if (response.ok) {
const headers = new Headers(response.headers);
headers.append('sw-cached-date', new Date().toISOString());
const timestamped = new Response(response.clone().body, {
status: response.status,
statusText: response.statusText,
headers,
});
await cache.put(request, timestamped);
}
return response;
} catch {
if (cached) return cached; // Return stale on network failure
throw new Error('Network failed and no cache available');
}
}
Network-First with Timeout
async function networkFirstWithTimeout(request, cacheName, timeout) {
const cache = await caches.open(cacheName);
try {
const response = await Promise.race([
fetch(request),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Network timeout')), timeout)
),
]);
if (response.ok) {
await cache.put(request, response.clone());
}
return response;
} catch {
const cached = await cache.match(request);
if (cached) return cached;
if (request.mode === 'navigate') {
return caches.match('/offline.html');
}
return new Response(
JSON.stringify({ error: 'Offline', cached: false }),
{ status: 503, headers: { 'Content-Type': 'application/json' } }
);
}
}
Stale-While-Revalidate
async function staleWhileRevalidate(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
const fetchPromise = fetch(request).then((response) => {
if (response.ok) cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}
Background Sync
Queue failed mutations for retry when connectivity resumes:
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-api-calls') {
event.waitUntil(retryFailedRequests());
}
});
async function retryFailedRequests() {
// Read queued requests from IndexedDB
const queue = await getQueuedRequests();
const results = await Promise.allSettled(
queue.map((req) => fetch(req.url, {
method: req.method,
headers: new Headers(req.headers),
body: req.body,
}))
);
// Remove successful, keep failed for next sync
const remaining = queue.filter((_, i) =>
results[i].status !== 'fulfilled' || !results[i].value.ok
);
await saveQueuedRequests(remaining);
// Notify open tabs
const clients = await self.clients.matchAll();
clients.forEach((client) => {
client.postMessage({ type: 'SYNC_COMPLETE', remaining: remaining.length });
});
}
React Registration
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js')
.then((registration) => {
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker?.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
showUpdateNotification(); // New version available
}
});
});
});
});
}
Critical Rules
| Rule | Why |
|---|---|
| Always clean old caches on activate | Storage bloats without cleanup |
Use skipWaiting() + clients.claim() | Immediate control of pages |
| Version bump cache names on deploy | Forces fresh asset fetch |
| Only cache GET requests | Mutations need idempotency guarantees |
| Provide offline fallback for navigation | Users see blank page otherwise |
| Test in incognito | Avoids stale SW from previous sessions |
Common Pitfalls
| Problem | Solution |
|---|---|
| SW serves stale content forever | Add cache expiry or version bumping |
| Infinite redirect loops | Exclude auth endpoints from caching |
| Cache fills up device storage | Set max cache size, prune old entries |
| SW blocks updates | Use skipWaiting() to activate immediately |
| API data stale after offline | Show "last updated" timestamp to users |
When not to use it
- →Mutations requiring idempotency
- →Authentication endpoints
Prerequisites
Limitations
- →Only cache GET requests
- →Requires version bumping for updates
How it compares
Provides structured, strategy-based routing for offline resilience rather than manual cache management.
Compared to similar skills
service-worker-offline-first side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| service-worker-offline-first (this skill) | 0 | 3mo | Review | Intermediate |
| web-games | 11 | 6mo | No flags | Intermediate |
| pwa-development | 9 | 4mo | Caution | Intermediate |
| vite | 7 | 5mo | Review | Advanced |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by fabioc-aloha
View all by fabioc-aloha →You might also like
web-games
davila7
Web browser game development principles. Framework selection, WebGPU, optimization, PWA.
pwa-development
alinaqi
Progressive Web Apps - service workers, caching strategies, offline, Workbox
vite
antfu
Vite build tool configuration, plugin API, SSR, and Vite 8 Rolldown migration. Use when working with Vite projects, vite.config.ts, Vite plugins, or building libraries/SSR apps with Vite.
webf-infinite-scrolling
openwebf
Create high-performance infinite scrolling lists with pull-to-refresh and load-more capabilities using WebFListView. Use when building feed-style UIs, product catalogs, chat messages, or any scrollable list that needs optimal performance with large datasets.
js-hoist-regexp
TheOrcDev
Hoist RegExp creation outside render or memoize with useMemo(). Apply when using regular expressions in React components or frequently called functions.
javascript-async-dom
sraloff
Best practices for Fetch API, event delegation, and DOM manipulation.