| name | caching-service-worker-caching |
| description | Progressive Web App (PWA) caching with Service Workers - offline-first architecture, caching strategies, Workbox patterns, and modern browser caching. |
Service Worker Caching
Last Updated: 2025-10-25
When to Use This Skill
Use this skill when:
- Building Progressive Web Apps (PWAs) with offline support
- Implementing client-side caching for web applications
- Creating offline-first or resilient web experiences
- Reducing server load with aggressive client caching
- Improving performance on slow or unreliable networks
- Building installable web apps with native-like experiences
Browser Support (2025): Chrome, Edge, Safari, Firefox (95%+ global coverage)
Prerequisites: Understanding of http-caching.md (HTTP caching headers complement Service Workers)
Core Concepts
Service Worker Overview
A Service Worker is a JavaScript file that runs in the background, separate from the web page:
- Intercepts network requests (acts as a programmable proxy)
- Manages a cache storage independent of HTTP cache
- Enables offline functionality
- Runs on a separate thread (doesn't block UI)
Key difference from HTTP cache: Service Worker cache is programmatic - you control every aspect of caching logic.
Service Worker Lifecycle
Install โ Activate โ Fetch (intercept requests)
const CACHE_VERSION = 'v1';
const CACHE_NAME = `app-cache-${CACHE_VERSION}`;
self.addEventListener('install', (event) => {
console.log('[SW] Installing service worker...');
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[SW] Caching app shell');
return cache.addAll([
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png',
]);
})
);
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
console.log('[SW] Activating service worker...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => {
console.log(`[SW] Deleting old cache: ${name}`);
return caches.delete(name);
})
);
})
);
return self.clients.claim();
});
self.addEventListener('fetch', (event) => {
console.log(`[SW] Fetching: ${event.request.url}`);
});
Registering Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js')
.then((registration) => {
console.log('SW registered:', registration.scope);
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
console.log('New service worker found, installing...');
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
console.log('New version available! Reload to update.');
showUpdateNotification();
}
});
});
})
.catch((error) => {
console.error('SW registration failed:', error);
});
});
}
function showUpdateNotification() {
if (confirm('New version available! Reload to update?')) {
window.location.reload();
}
}
Caching Strategies
1. Cache First (Cache Falling Back to Network)
Best for: Static assets (JS, CSS, images) that rarely change.
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
console.log(`[SW] Cache hit: ${event.request.url}`);
return cachedResponse;
}
console.log(`[SW] Cache miss, fetching: ${event.request.url}`);
return fetch(event.request).then((networkResponse) => {
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
});
})
);
});
2. Network First (Network Falling Back to Cache)
Best for: Dynamic content (API responses) where freshness is important.
function networkFirst(request) {
return fetch(request)
.then((networkResponse) => {
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, networkResponse.clone());
});
return networkResponse;
})
.catch(() => {
console.log(`[SW] Network failed, using cache: ${request.url}`);
return caches.match(request);
});
}
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(networkFirst(event.request));
}
});
3. Stale-While-Revalidate
Best for: Balancing freshness and performance - serve cached, update in background.
function staleWhileRevalidate(request) {
return caches.open(CACHE_NAME).then((cache) => {
return cache.match(request).then((cachedResponse) => {
const fetchPromise = fetch(request).then((networkResponse) => {
cache.put(request, networkResponse.clone());
return networkResponse;
});
return cachedResponse || fetchPromise;
});
});
}
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/articles/')) {
event.respondWith(staleWhileRevalidate(event.request));
}
});
4. Network Only
Best for: POST requests, analytics, real-time data.
self.addEventListener('fetch', (event) => {
if (event.request.method === 'POST') {
event.respondWith(fetch(event.request));
}
});
5. Cache Only
Best for: App shell that's guaranteed to be cached.
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/app-shell/')) {
event.respondWith(caches.match(event.request));
}
});
Workbox Library (Google's Service Worker Framework)
Workbox simplifies Service Worker development with pre-built strategies.
Basic Workbox Setup
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js');
const { registerRoute } = workbox.routing;
const { CacheFirst, NetworkFirst, StaleWhileRevalidate } = workbox.strategies;
const { CacheableResponsePlugin } = workbox.cacheableResponse;
const { ExpirationPlugin } = workbox.expiration;
registerRoute(
({ request }) => request.mode === 'navigate',
new NetworkFirst({
cacheName: 'pages',
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
],
})
);
registerRoute(
({ request }) =>
request.destination === 'style' ||
request.destination === 'script' ||
request.destination === 'worker',
new StaleWhileRevalidate({
cacheName: 'assets',
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
],
})
);
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60,
}),
],
})
);
registerRoute(
({ url }) => url.origin === 'https://fonts.googleapis.com',
new StaleWhileRevalidate({
cacheName: 'google-fonts-stylesheets',
})
);
registerRoute(
({ url }) => url.origin === 'https://fonts.gstatic.com',
new CacheFirst({
cacheName: 'google-fonts-webfonts',
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxAgeSeconds: 60 * 60 * 24 * 365,
maxEntries: 30,
}),
],
})
);
Workbox with Build Tools (Vite/Webpack)
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60,
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
],
},
}),
],
});
Offline-First Architecture
Offline Page Fallback
const OFFLINE_PAGE = '/offline.html';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.add(OFFLINE_PAGE);
})
);
});
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(OFFLINE_PAGE);
})
);
}
});
Background Sync for Failed Requests
Queue failed requests, retry when online.
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-posts') {
event.waitUntil(syncPendingPosts());
}
});
async function syncPendingPosts() {
const cache = await caches.open('pending-posts');
const requests = await cache.keys();
return Promise.all(
requests.map(async (request) => {
try {
const response = await fetch(request.clone());
if (response.ok) {
await cache.delete(request);
console.log('Synced pending post:', request.url);
}
} catch (error) {
console.error('Sync failed:', error);
}
})
);
}
async function submitPost(data) {
try {
const response = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
});
return response;
} catch (error) {
const cache = await caches.open('pending-posts');
await cache.put(
new Request('/api/posts', {
method: 'POST',
body: JSON.stringify(data),
})
);
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-posts');
console.log('Post queued for background sync');
}
}
Cache Versioning and Migration
Versioned Caches
const CACHE_VERSION = 'v2';
const CACHE_NAME = `app-cache-${CACHE_VERSION}`;
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name.startsWith('app-cache-') && name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
})
);
});
Migration Between Versions
self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
const oldCacheName = 'app-cache-v1';
const newCacheName = 'app-cache-v2';
if (await caches.has(oldCacheName)) {
const oldCache = await caches.open(oldCacheName);
const newCache = await caches.open(newCacheName);
const requests = await oldCache.keys();
for (const request of requests) {
if (request.url.includes('/static/')) {
const response = await oldCache.match(request);
await newCache.put(request, response);
}
}
await caches.delete(oldCacheName);
console.log('Cache migrated from v1 to v2');
}
})()
);
});
Advanced Patterns
Conditional Caching
self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') {
return;
}
event.respondWith(
caches.match(event.request).then((cached) => {
return (
cached ||
fetch(event.request).then((response) => {
if (response.status === 200) {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
}
return response;
})
);
})
);
});
Cache with Timeout
function fetchWithTimeout(request, timeout = 3000) {
return Promise.race([
fetch(request),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Network timeout')), timeout)
),
]);
}
self.addEventListener('fetch', (event) => {
event.respondWith(
fetchWithTimeout(event.request, 2000)
.catch(() => caches.match(event.request))
.catch(() => caches.match('/offline.html'))
);
});
Debugging Service Workers
Chrome DevTools
const DEBUG = true;
function log(...args) {
if (DEBUG) {
console.log('[SW]', ...args);
}
}
self.addEventListener('fetch', (event) => {
log('Fetch:', event.request.url);
log('Cache strategy:', getStrategy(event.request.url));
});
Chrome DevTools:
- Open DevTools โ Application tab โ Service Workers
- Check "Update on reload" for development
- "Unregister" to remove service worker
- "Bypass for network" to disable temporarily
- Application โ Cache Storage to inspect cached data
Force Update
navigator.serviceWorker.register('/service-worker.js').then((registration) => {
setInterval(() => {
registration.update();
}, 60 * 60 * 1000);
});
document.getElementById('update-btn').addEventListener('click', () => {
navigator.serviceWorker.ready.then((registration) => {
registration.update();
});
});
Anti-Patterns
โ Caching Everything
self.addEventListener('fetch', (event) => {
event.respondWith(caches.match(event.request) || fetch(event.request));
});
if (event.request.method === 'GET' && !event.request.url.includes('/api/user')) {
}
โ Not Versioning Caches
const CACHE_NAME = 'app-cache';
const CACHE_VERSION = 'v2';
const CACHE_NAME = `app-cache-${CACHE_VERSION}`;
โ Forgetting to Clean Up
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(names.filter((n) => n !== CACHE_NAME).map((n) => caches.delete(n)))
)
);
});
Quick Reference
Caching Strategy Selection:
Static assets (JS/CSS/images) โ Cache First
API responses โ Network First
Semi-dynamic content โ Stale While Revalidate
Real-time data / POST requests โ Network Only
App shell โ Cache Only
Cache Size Limits (2025):
- Chrome: ~60% of free disk space
- Safari: 50MB soft limit, up to 1GB with user permission
- Firefox: Up to 50% of free disk space
Service Worker Lifecycle:
- Register โ
navigator.serviceWorker.register()
- Install โ
self.addEventListener('install')
- Activate โ
self.addEventListener('activate')
- Fetch โ
self.addEventListener('fetch')
Workbox Strategies:
CacheFirst: Static assets
NetworkFirst: Dynamic content
StaleWhileRevalidate: Balance freshness/performance
NetworkOnly: Real-time data
CacheOnly: Guaranteed cached content
Related Skills
http-caching.md - HTTP caching headers (complement Service Workers)
frontend-performance.md - Overall frontend performance optimization
web-accessibility.md - Ensure offline pages are accessible
cache-invalidation-strategies.md - Cache versioning strategies
Summary
Service Workers provide powerful client-side caching for Progressive Web Apps:
Key Takeaways:
- Service Workers โ HTTP Cache - They complement each other
- Choose the right strategy - Cache First for static, Network First for dynamic
- Use Workbox for production - Handles edge cases and best practices
- Version your caches - Clean up old caches on activation
- Offline-first thinking - Plan for network failures
- Background Sync - Queue failed requests for retry when online
- Debug thoroughly - Service Workers can cause hard-to-debug issues
Service Workers enable resilient, performant web applications that work offline. Combined with HTTP caching and CDN edge caching, they form a complete caching strategy from browser to origin server.