Offline-First App with Service Workers
A Service Worker is a background JavaScript file that intercepts network requests, making it possible to serve your simulation from cache when there is no internet connection. This tutorial covers the SW lifecycle, cache-first and stale-while-revalidate strategies, the Web App Manifest for home-screen installability, and a practical comparison of vanilla SW vs Workbox.
- Basic JavaScript (async/await, promises, fetch)
- Understanding of browser caching at a conceptual level
-
A local server (Live Server extension,
npx serve) — SW requires HTTPS or localhost
Service Worker Lifecycle
Understanding the lifecycle prevents the #1 SW confusion: why aren't my changes showing?
-
Register — main page calls
navigator.serviceWorker.register('/sw.js'). The browser downloads and parses the SW file. -
Install — SW fires
installevent. Use this to pre-cache assets. The SW is "installed" but not yet "active". -
Wait — if an old SW is still controlling pages, the
new SW waits. Call
self.skipWaiting()to skip this. -
Activate — SW takes control. Use this to clean up
old caches. Call
clients.claim()to take control of existing pages immediately. - Fetch — SW intercepts every network request from controlled pages. You decide: serve from cache, fetch from network, or both.
To force-update a SW during development: DevTools → Application →
Service Workers → "Update on reload" and "Bypass for network". Or
change the CACHE_VERSION constant to trigger
install/activate.
Register the SW from Your Page
// In your main HTML or main JS file
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const reg = await navigator.serviceWorker.register('/sw.js', {
scope: '/', // SW controls all pages under /
});
console.log('SW registered, scope:', reg.scope);
// Optional: listen for updates
reg.addEventListener('updatefound', () => {
const newSW = reg.installing;
newSW.addEventListener('statechange', () => {
if (newSW.state === 'installed' && navigator.serviceWorker.controller) {
// New content available — show user a "Refresh" prompt
console.log('New version available!');
}
});
});
} catch (err) {
console.error('SW registration failed:', err);
}
});
}
Install Event — Precache Assets
On install, fetch and store every critical asset. The SW won't activate until all assets are cached successfully:
// sw.js
const CACHE_VERSION = 'v2'; // bump this to trigger update
const CACHE_NAME = `sim-cache-${CACHE_VERSION}`;
const PRECACHE_ASSETS = [
'/',
'/index.html',
'/offline.html',
'/shared/theme.css',
'/shared/components.css',
'/shared/components.js',
'/manifest.json',
// Add your simulation HTML files:
'/fluid/index.html',
'/pathfinding/index.html',
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
console.log('Precaching assets...');
return cache.addAll(PRECACHE_ASSETS);
})
);
self.skipWaiting(); // Don't wait — take over immediately
});
Fetch Event — Cache-First Strategy
Cache-first: try the cache. If found, return immediately. If not, fetch from network and cache the response for next time. Best for static assets:
self.addEventListener('fetch', event => {
// Only handle GET requests
if (event.request.method !== 'GET') return;
event.respondWith(cacheFirst(event.request));
});
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached; // ← serve from cache instantly
try {
const response = await fetch(request);
if (response.ok) {
// Cache a clone (responses can only be consumed once)
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
} catch {
// Completely offline and not in cache
return caches.match('/offline.html');
}
}
Cache-First
Serve from cache. Fast, works offline. Stale until SW updates. Use for: JS, CSS, fonts.
Network-First
Try network. Fall back to cache on failure. Fresh when online, usable offline. Use for: API, HTML.
Stale-While-Revalidate
Serve cache immediately, then update cache in background. Always fast, eventually fresh. Use for: avatars, non-critical assets.
Network-Only
Always fetch. Never cache. Use for: analytics, payments, anything that must be fresh.
Activate — Clean Old Caches
When a new SW activates, delete caches from previous versions to free storage:
self.addEventListener('activate', event => {
event.waitUntil(
Promise.all([
// Delete all caches except current version
caches.keys().then(keys =>
Promise.all(
keys
.filter(key => key !== CACHE_NAME)
.map(key => {
console.log('Deleting old cache:', key);
return caches.delete(key);
})
)
),
// Take control of all open tabs immediately
self.clients.claim(),
])
);
});
Stale-While-Revalidate Strategy
Serve the cached version instantly, then update the cache in the background. The user gets fast response and the next visit gets fresh content:
async function staleWhileRevalidate(request) {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
// Revalidate in background regardless of cache hit
const fetchPromise = fetch(request).then(response => {
if (response.ok) cache.put(request, response.clone());
return response;
}).catch(() => null); // ignore failure if offline
// Return cached immediately, or wait for network if no cache
return cached ?? fetchPromise;
}
// Use different strategies for different request types:
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// CSS/JS: cache-first (versioned, rarely changes)
if (url.pathname.match(/\.(css|js)$/)) {
event.respondWith(cacheFirst(event.request));
return;
}
// HTML pages: stale-while-revalidate
if (event.request.headers.get('accept')?.includes('text/html')) {
event.respondWith(staleWhileRevalidate(event.request));
return;
}
// Everything else: network-first
event.respondWith(networkFirst(event.request));
});
Web App Manifest — Make It Installable
With a valid manifest and a registered SW, Chrome/Edge show an "Install" prompt. The app can be added to the home screen and opens full-screen with no browser chrome:
// manifest.json
{
"name": "3D Simulations",
"short_name": "3DSims",
"description": "Interactive physics and algorithm simulations",
"start_url": "/",
"display": "standalone",
"background_color": "#0a0a10",
"theme_color": "#22c55e",
"orientation": "any",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icons/icon-512-maskable.png", "sizes": "512x512",
"type": "image/png", "purpose": "maskable" }
],
"screenshots": [
{ "src": "/screenshots/fluid.jpg", "sizes": "1280x720", "type": "image/jpeg",
"form_factor": "wide" }
]
}
<!-- In <head> of every page: -->
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#22c55e" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
PWA checklist: SW must be on HTTPS (or localhost),
manifest must have valid icons, start_url must be in
the SW scope. Check with Lighthouse (DevTools → Lighthouse →
Progressive Web App) for full audit.
Frequently Asked Questions
What will I learn in this tutorial?
Make your simulation work offline using a Service Worker: cache-first strategy, background sync, Workbox vs vanilla SW, and the Web App Manifest for installability.
What topics are covered in this tutorial?
This tutorial covers: Service Worker lifecycle, Register the SW from your page, Install event — precache assets, Fetch event — cache-first strategy, Activate — clean old caches, Stale-while-revalidate strategy, Web App Manifest — make it installable.
What tools and technologies does this tutorial use?
This tutorial uses Service Workers, PWA, JavaScript.
How long does this tutorial take?
This tutorial takes approximately 45 minutes to complete.
What prerequisites do I need before starting?
Before starting, you should have: Basic JavaScript (async/await, promises, fetch); Understanding of browser caching at a conceptual level; A local server (Live Server extension, npx serve ) — SW requires HTTPS or localhost.