Progressive Web Apps (PWA)
Caching Strategies
Understanding Caching Strategies
Caching strategies determine how your service worker responds to network requests. Different strategies work better for different types of resources. The right strategy can dramatically improve performance and enable offline functionality.
Why Multiple Strategies?
No single caching strategy fits all resources:
- Static assets (CSS, JS) rarely change - cache first
- API data changes frequently - network first
- Images are large but static - cache first with fallback
- User content must be fresh - network only or network first
1. Cache First (Cache Falling Back to Network)
Check cache first, only fetch from network if not cached. Best for static assets that rarely change.
// sw.js - Cache First Strategy
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request);
})
);
});
Cache First - Best For:
Cons: May serve stale content
- Static CSS and JavaScript files
- Images and fonts
- HTML templates (if using SPA)
- Any versioned assets (app-v1.2.3.js)
Cons: May serve stale content
2. Network First (Network Falling Back to Cache)
// sw.js - Network First Strategy
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request)
.then(response => {
return caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, response.clone());
return response;
});
})
.catch(() => caches.match(event.request))
);
});
Network First - Best For: API responses, dynamic data, news feeds
Pros: Always fresh when online
Cons: Slower, uses bandwidth
Pros: Always fresh when online
Cons: Slower, uses bandwidth
3. Stale While Revalidate
// sw.js - Stale While Revalidate
self.addEventListener('fetch', event => {
event.respondWith(
caches.open(CACHE_NAME).then(cache => {
return cache.match(event.request).then(response => {
const fetchPromise = fetch(event.request).then(networkResponse => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return response || fetchPromise;
});
})
);
});
Best balance: Returns cached instantly, updates in background
4. Network Only & Cache Only
// Network Only (for auth, payments)
event.respondWith(fetch(event.request));
// Cache Only (rarely used)
event.respondWith(caches.match(event.request));
Choosing the Right Strategy
| Resource | Strategy |
|---|---|
| CSS/JS/Fonts | Cache First |
| API Data | Network First |
| Images | Stale While Revalidate |
| Authentication | Network Only |
Complete Multi-Strategy Example
// sw.js - Combined Strategies
const CACHE_NAME = 'pwa-v1';
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// Network Only - Authentication
if (url.pathname.startsWith('/api/auth')) {
event.respondWith(fetch(event.request));
return;
}
// Network First - API
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(event.request)
.then(response => {
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, response.clone());
});
return response;
})
.catch(() => caches.match(event.request))
);
return;
}
// Stale While Revalidate - Images
if (event.request.destination === 'image') {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
const fetchPromise = fetch(event.request).then(response => {
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, response.clone());
});
return response;
});
return cachedResponse || fetchPromise;
})
);
return;
}
// Cache First - Everything else
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
Cache Management
// Limit cache size
const MAX_CACHE_SIZE = 50;
async function trimCache(cacheName, maxItems) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
if (keys.length > maxItems) {
await cache.delete(keys[0]);
await trimCache(cacheName, maxItems);
}
}
// Clean old caches on activate
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(names => {
return Promise.all(
names
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
});
Offline Fallback
// Cache offline page during install
const OFFLINE_PAGE = '/offline.html';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.add(OFFLINE_PAGE))
);
});
// Show offline page on navigation failure
self.addEventListener('fetch', event => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.catch(() => caches.match(OFFLINE_PAGE))
);
}
});
Cache Storage Limits:
- Chrome: Up to 60% of disk space
- Firefox: Up to 50% of free space
- Safari: Up to 50MB (can request more)
navigator.storage.estimate()
Exercise:
- Create a service worker with all 5 caching strategies
- Apply Cache First for CSS/JS files
- Apply Network First for /api/* endpoints
- Apply Stale While Revalidate for images
- Implement cache size limit (max 20 items)
- Add offline fallback page
- Test using Chrome DevTools Network tab (Offline mode)
- Inspect Cache Storage in Application tab
- Measure cache usage with navigator.storage.estimate()