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:

bash
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 API

The 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 install event 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. 1.Version your cache names in the Service Worker file:
  2. 2.```javascript
  3. 3.const CACHE_NAME = 'myapp-v2026-04-09-1';
  4. 4.const ASSETS_TO_CACHE = [
  5. 5.'/',
  6. 6.'/index.html',
  7. 7.'/app.css',
  8. 8.'/app.js',
  9. 9.];

self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll(ASSETS_TO_CACHE); }) ); self.skipWaiting(); // Activate immediately }); ```

  1. 1.Clean up old caches during activation:
  2. 2.```javascript
  3. 3.self.addEventListener('activate', (event) => {
  4. 4.event.waitUntil(
  5. 5.caches.keys().then((cacheNames) => {
  6. 6.return Promise.all(
  7. 7.cacheNames
  8. 8..filter((name) => name !== CACHE_NAME)
  9. 9..map((name) => caches.delete(name))
  10. 10.);
  11. 11.})
  12. 12.);
  13. 13.return self.clients.claim(); // Take control of all open pages immediately
  14. 14.});
  15. 15.`
  16. 16.Use a network-first strategy for HTML navigation requests:
  17. 17.```javascript
  18. 18.self.addEventListener('fetch', (event) => {
  19. 19.if (event.request.mode === 'navigate') {
  20. 20.event.respondWith(
  21. 21.fetch(event.request)
  22. 22..then((response) => {
  23. 23.const cache = caches.open(CACHE_NAME);
  24. 24.cache.put(event.request, response.clone());
  25. 25.return response;
  26. 26.})
  27. 27..catch(() => caches.match(event.request))
  28. 28.);
  29. 29.}
  30. 30.});
  31. 31.`
  32. 32.Generate cache-busting filenames in your build process. For Webpack:
  33. 33.```javascript
  34. 34.module.exports = {
  35. 35.output: {
  36. 36.filename: '[name].[contenthash].js',
  37. 37.chunkFilename: '[name].[contenthash].chunk.js',
  38. 38.},
  39. 39.};
  40. 40.`
  41. 41.This produces files like main.a1b2c3d4.js that automatically bust the Service Worker cache.
  42. 42.Implement an update notification to inform users of a new version:
  43. 43.```javascript
  44. 44.navigator.serviceWorker.addEventListener('controllerchange', () => {
  45. 45.if (window.location.reload) {
  46. 46.// Show a toast notification
  47. 47.showUpdateNotification('New version available. Refreshing...');
  48. 48.setTimeout(() => window.location.reload(), 2000);
  49. 49.}
  50. 50.});
  51. 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() and clients.claim() carefully - they can cause mid-session JavaScript version mismatches