Progressive Web Apps (PWA)

Caching Strategies

20 min Lesson 5 of 30

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:
  • Static CSS and JavaScript files
  • Images and fonts
  • HTML templates (if using SPA)
  • Any versioned assets (app-v1.2.3.js)
Pros: Fastest response, works offline, reduces bandwidth
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

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)
Check usage: navigator.storage.estimate()
Exercise:
  1. Create a service worker with all 5 caching strategies
  2. Apply Cache First for CSS/JS files
  3. Apply Network First for /api/* endpoints
  4. Apply Stale While Revalidate for images
  5. Implement cache size limit (max 20 items)
  6. Add offline fallback page
  7. Test using Chrome DevTools Network tab (Offline mode)
  8. Inspect Cache Storage in Application tab
  9. Measure cache usage with navigator.storage.estimate()