
Service Worker Offline First
- 1 installs
- 3 repo stars
- Updated August 5, 2026
- fabioc-aloha/alex_skill_mall
Provides PWA offline-first patterns covering Service Worker lifecycle, caching strategies, background sync, and offline fallbacks.
About
Covers Progressive Web App offline-first patterns including Service Worker lifecycle, caching-strategy selection, background sync, and offline fallbacks. Developers use it to make web apps resilient when the network is unavailable.
- Caching-strategy selection table (cache-first, network-first, SWR)
- Background sync and offline fallback patterns
Service Worker Offline First by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,914 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/fabioc-aloha/alex_skill_mall --skill service-worker-offline-firstAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 3 |
| Last updated | August 5, 2026 |
| Repository | fabioc-aloha/alex_skill_mall ↗ |
What it does
Provides PWA offline-first patterns covering Service Worker lifecycle, caching strategies, background sync, and offline fallbacks.
Files
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 typeCore 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 |
{
"name": "service-worker-offline-first",
"version": "1.0.0",
"description": "Progressive Web App offline-first patterns -- Service Worker lifecycle, caching strategies, background sync, and offline fallbacks for resilient web applications",
"author": {
"name": "ACT Plugin Mall"
},
"keywords": [
"service",
"worker",
"offline",
"first",
"progressive",
"offline-first",
"patterns",
"lifecycle"
],
"engines": [
"copilot"
],
"shape": ".S..",
"category": "platform-tooling",
"tier": "standard",
"token_cost": 2065,
"requires_edition": ">=1.0.0",
"requires_plugins": [],
"artifacts": {
"skill": "SKILL.md"
},
"install_paths": {
"skill": ".github/skills/local/service-worker-offline-first/SKILL.md"
}
}
service-worker-offline-first
Progressive Web App offline-first patterns -- Service Worker lifecycle, caching strategies, background sync, and offline fallbacks for resilient web applications
Quick Facts
| Field | Value |
|---|---|
| Category | platform-tooling |
| Shape | .S.. |
| Tier | standard |
| Token cost | ~2065 tokens |
| Engines | copilot |
| Requires Edition | >=1.0.0 |
| Keywords | service, worker, offline, first, progressive, offline-first, patterns, lifecycle |
Installation
From ACT Edition:
/mall install service-worker-offline-firstOr manually copy the plugin contents to .github/skills/local/service-worker-offline-first/.
Artifacts
- skill:
SKILL.md
Shape: .S..
Skill only -- domain knowledge loaded when pattern matches.