What is advanced caching strategy?
Advanced caching strategies in JavaScript, primarily implemented using Service Workers, go beyond traditional browser caching to provide robust performance improvements, offline capabilities, and a more resilient user experience. These strategies dictate how a web application should fetch resources, prioritizing speed, freshness, or offline availability.
The Foundation: Service Workers
Service Workers are programmable proxies that sit between web applications and the network. They can intercept network requests, cache responses, and serve them from the cache, enabling fine-grained control over caching behavior. This is crucial for implementing advanced strategies.
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('ServiceWorker registered: ', registration);
})
.catch(error => {
console.error('ServiceWorker registration failed: ', error);
});
});
}
Common Caching Strategies
1. Cache-First (Cache-Only / Cache-then-Network)
This strategy attempts to retrieve a resource from the cache first. If it's found, it's served immediately. If not, it falls back to the network, fetches the resource, and then caches it for future use. This is ideal for static assets that don't change often.
// In sw.js
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache hit - return response
if (response) {
return response;
}
// No cache match - fetch from network
return fetch(event.request).then(networkResponse => {
return caches.open('my-cache').then(cache => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
});
})
);
});
2. Network-First (Network-Only / Network-then-Cache)
This strategy tries to fetch the resource from the network first. If the network request succeeds, the resource is used and optionally cached. If the network request fails (e.g., offline), it falls back to the cache. This is good for frequently updated resources where freshness is paramount, but offline availability is still desired.
// In sw.js
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request)
.then(networkResponse => {
return caches.open('my-cache').then(cache => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
})
.catch(() => {
// Network failed, try cache
return caches.match(event.request);
})
);
});
3. Stale-While-Revalidate
This strategy serves the content from the cache immediately (stale content), but also makes a network request in the background to get the latest version. Once the network request completes, it updates the cache for future requests. This provides a fast user experience while ensuring content eventually gets updated. It's excellent for APIs or news feeds.
// In sw.js
self.addEventListener('fetch', event => {
event.respondWith(
caches.open('my-cache').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;
});
})
);
});
4. Cache-Only
Only serves resources from the cache. It never goes to the network. This is useful for pre-cached essential files during service worker installation, ensuring they are always available offline.
// In sw.js (during install)
const precacheAssets = [
'/',
'/index.html',
'/styles.css',
'/app.js'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open('static-cache-v1').then(cache => {
return cache.addAll(precacheAssets);
})
);
});
// In sw.js (during fetch)
self.addEventListener('fetch', event => {
if (precacheAssets.includes(event.request.url.replace(self.location.origin, ''))) {
event.respondWith(caches.match(event.request));
}
});
5. Network-Only
Always goes to the network and never uses the cache. This is suitable for requests that should never be cached, like analytics pings or highly dynamic, real-time data.
// In sw.js
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/realtime-data')) {
event.respondWith(fetch(event.request));
}
});
Cache Management and Versioning
Effective caching requires managing cache versions to ensure users get updated content and to clean up old, unused caches. This often involves incrementing a cache name (e.g., my-cache-v2) and deleting older caches during the activate event of the service worker.
// In sw.js
const CACHE_NAME = 'my-app-cache-v2';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
IndexedDB Integration
For larger amounts of structured data, or data that needs to be queried, IndexedDB can be used in conjunction with Service Workers. Service Workers can intercept API requests, store responses in IndexedDB, and serve them from there when offline or in a cache-first scenario. This allows for rich offline experiences beyond just static assets.
Tooling: Workbox
Implementing these strategies manually can be complex and error-prone. Google's Workbox is a set of libraries that simplify service worker development, providing pre-built modules for common caching patterns (like the ones described above), cache expiration, routing, and precaching.
// Using Workbox (in sw.js)
import {registerRoute} from 'workbox-routing';
import {CacheFirst, NetworkFirst, StaleWhileRevalidate} from 'workbox-strategies';
import {CacheableResponsePlugin} from 'workbox-cacheable-response';
import {ExpirationPlugin} from 'workbox-expiration';
// Cache-First for images
registerRoute(
({request}) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
}),
],
})
);
// Stale-While-Revalidate for API calls
registerRoute(
({url}) => url.pathname.startsWith('/api/'),
new StaleWhileRevalidate({
cacheName: 'api-responses',
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 5 * 60, // 5 Minutes
}),
],
})
);