Skip to main content

Service Workers and Offline HTML Caching

Answer

Service Workers are JavaScript scripts that run in the background, separate from web pages, enabling features like offline support, push notifications, and background sync.

Service Worker Lifecycle

Registering a Service Worker

// In your main JavaScript file
if ("serviceWorker" in navigator) {
window.addEventListener("load", async () => {
try {
const registration = await navigator.serviceWorker.register("/sw.js", {
scope: "/",
});
console.log("SW registered:", registration.scope);
} catch (error) {
console.log("SW registration failed:", error);
}
});
}

Basic Service Worker (sw.js)

const CACHE_NAME = "v1";
const urlsToCache = [
"/",
"/index.html",
"/styles.css",
"/app.js",
"/images/logo.png",
"/offline.html",
];

// Install event - cache resources
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log("Caching app shell");
return cache.addAll(urlsToCache);
})
);
// Activate immediately
self.skipWaiting();
});

// Activate event - clean old caches
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
})
);
// Take control immediately
self.clients.claim();
});

// Fetch event - serve from cache
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});

Caching Strategies

Cache First Strategy

// Best for: CSS, JS, images, fonts
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}

return fetch(event.request).then((networkResponse) => {
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
});
})
);
});

Network First Strategy

// Best for: API calls, frequently updated content
self.addEventListener("fetch", (event) => {
if (event.request.url.includes("/api/")) {
event.respondWith(
fetch(event.request)
.then((response) => {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => {
return caches.match(event.request);
})
);
}
});

Stale While Revalidate

// Best for: User data, social feeds
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.open(CACHE_NAME).then((cache) => {
return cache.match(event.request).then((cachedResponse) => {
const fetchPromise = fetch(event.request).then((networkResponse) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});

return cachedResponse || fetchPromise;
});
})
);
});

Offline Fallback Page

// sw.js
self.addEventListener("fetch", (event) => {
if (event.request.mode === "navigate") {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match("/offline.html");
})
);
}
});
<!-- offline.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Offline - Site Name</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: system-ui;
}
.offline-message {
text-align: center;
}
</style>
</head>
<body>
<div class="offline-message">
<h1>You're Offline</h1>
<p>Please check your connection and try again.</p>
<button onclick="location.reload()">Retry</button>
</div>
</body>
</html>

Update Service Worker

// Register with update check
const registration = await navigator.serviceWorker.register("/sw.js");

// Check for updates every hour
setInterval(() => {
registration.update();
}, 60 * 60 * 1000);

// Listen for new service worker
registration.addEventListener("updatefound", () => {
const newWorker = registration.installing;

newWorker.addEventListener("statechange", () => {
if (newWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// New content available
showUpdateNotification();
}
}
});
});

Debugging

// Check registration status
navigator.serviceWorker.getRegistration().then((reg) => {
console.log("SW state:", reg?.active?.state);
});

// Clear all caches
caches.keys().then((names) => {
names.forEach((name) => caches.delete(name));
});

// Unregister service worker
navigator.serviceWorker.getRegistrations().then((regs) => {
regs.forEach((reg) => reg.unregister());
});

Key Points

  • Service Workers run in background, separate from page
  • Must be served over HTTPS (except localhost)
  • Control caching strategies for offline support
  • Lifecycle: install → waiting → activate
  • Use skipWaiting() for immediate activation
  • Multiple caching strategies for different content types