Introduction
Service Workers aggressively cache static assets to enable offline functionality, but this creates a problem after deployments: users may continue to see old versions of CSS, JavaScript, and HTML files cached by the previous Service Worker. The symptoms include:
Browser shows outdated page content even after deployment
Console error: "Uncaught TypeError: Cannot read property 'xyz' of undefined"
from an old JavaScript file that no longer matches the server's APIThe root cause is that the Service Worker's cache was not properly invalidated when new assets were deployed.
Symptoms
- Users see outdated page content after a new deployment
- JavaScript errors from mismatched code versions (old JS calling new API)
- CSS styling appears broken or inconsistent
- Hard refresh (Ctrl+Shift+R) fixes the issue, confirming cache is the cause
- Some users see the new version while others remain on the old version
Common Causes
- Service Worker
installevent caches assets with static filenames (e.g.,app.js,styles.css) - Cache name does not include a version identifier, so the same cache key is reused
- Service Worker update is installed but not activated (waiting for all tabs to close)
- Navigation requests are served from cache without checking the network first
- Deployment changes asset content but not the Service Worker file itself
Step-by-Step Fix
- 1.Version your cache names in the Service Worker file:
- 2.```javascript
- 3.const CACHE_NAME = 'myapp-v2026-04-09-1';
- 4.const ASSETS_TO_CACHE = [
- 5.'/',
- 6.'/index.html',
- 7.'/app.css',
- 8.'/app.js',
- 9.];
self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll(ASSETS_TO_CACHE); }) ); self.skipWaiting(); // Activate immediately }); ```
- 1.Clean up old caches during activation:
- 2.```javascript
- 3.self.addEventListener('activate', (event) => {
- 4.event.waitUntil(
- 5.caches.keys().then((cacheNames) => {
- 6.return Promise.all(
- 7.cacheNames
- 8..filter((name) => name !== CACHE_NAME)
- 9..map((name) => caches.delete(name))
- 10.);
- 11.})
- 12.);
- 13.return self.clients.claim(); // Take control of all open pages immediately
- 14.});
- 15.
` - 16.Use a network-first strategy for HTML navigation requests:
- 17.```javascript
- 18.self.addEventListener('fetch', (event) => {
- 19.if (event.request.mode === 'navigate') {
- 20.event.respondWith(
- 21.fetch(event.request)
- 22..then((response) => {
- 23.const cache = caches.open(CACHE_NAME);
- 24.cache.put(event.request, response.clone());
- 25.return response;
- 26.})
- 27..catch(() => caches.match(event.request))
- 28.);
- 29.}
- 30.});
- 31.
` - 32.Generate cache-busting filenames in your build process. For Webpack:
- 33.```javascript
- 34.module.exports = {
- 35.output: {
- 36.filename: '[name].[contenthash].js',
- 37.chunkFilename: '[name].[contenthash].chunk.js',
- 38.},
- 39.};
- 40.
` - 41.This produces files like
main.a1b2c3d4.jsthat automatically bust the Service Worker cache. - 42.Implement an update notification to inform users of a new version:
- 43.```javascript
- 44.navigator.serviceWorker.addEventListener('controllerchange', () => {
- 45.if (window.location.reload) {
- 46.// Show a toast notification
- 47.showUpdateNotification('New version available. Refreshing...');
- 48.setTimeout(() => window.location.reload(), 2000);
- 49.}
- 50.});
- 51.
`
Prevention
- Always change the Service Worker file itself when deploying new assets (even a comment change triggers browser detection)
- Use Workbox library for production-ready caching strategies instead of writing custom Service Worker code
- Implement cache versioning with timestamp or git commit hash in the cache name
- Test Service Worker updates by deploying to staging and verifying old cache is cleaned up
- Monitor
caches.keys()in production to detect orphaned caches consuming storage - Use
skipWaiting()andclients.claim()carefully - they can cause mid-session JavaScript version mismatches