Cache-first for static assets. Network-first for inventory pages. The version string in sw.js is your deploy lever — increment it every time CSS or JS changes.
A service worker is a JavaScript file that runs in the background, separate from your page, and intercepts every network request the page makes. It’s what makes a PWA fast and offline-capable. Without a service worker, a PWA is just a website with a manifest.json.
The Anchor site’s mobile performance was acceptable but not excellent. Repeat visitors were re-downloading the same CSS, JS, and images on every visit. At the marina — exactly where buyers make decisions — cell signal is unreliable. A visitor who loaded the inventory page but then lost signal should still be able to browse what they already loaded.
The service worker is registered from a script included on every page. It registers on first load and updates silently when a new version is detected:
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then(function(reg) {
reg.addEventListener('updatefound', function() {
var newWorker = reg.installing;
newWorker.addEventListener('statechange', function() {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
console.log('New version available');
}
});
});
});
});
}
Different content types need different caching strategies. Static assets (CSS, JS, images) use cache-first. Dynamic content (inventory pages, search results) uses network-first with a cache fallback:
var CACHE_VERSION = 'v4'; // Increment to invalidate all caches
var STATIC_CACHE = 'static-' + CACHE_VERSION;
var DYNAMIC_CACHE = 'dynamic-' + CACHE_VERSION;
var STATIC_ASSETS = [
'/',
'/css/style.css',
'/js/site.js',
'/images/nav-logo.png',
'/offline.html',
];
// Install: pre-cache static assets
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(STATIC_CACHE).then(function(cache) {
return cache.addAll(STATIC_ASSETS);
})
);
self.skipWaiting();
});
// Activate: delete old cache versions
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(keys) {
return Promise.all(
keys
.filter(function(key) { return key !== STATIC_CACHE && key !== DYNAMIC_CACHE; })
.map(function(key) { return caches.delete(key); })
);
})
);
self.clients.claim();
});
// Fetch: cache-first for static, network-first for everything else
self.addEventListener('fetch', function(event) {
var url = new URL(event.request.url);
// Don't intercept non-GET, admin, API, or WP requests
if (event.request.method !== 'GET'
|| url.pathname.startsWith('/wp-admin')
|| url.pathname.startsWith('/wp-json')
|| url.search.includes('?nocache')) {
return;
}
// Cache-first for static assets
if (url.pathname.match(/\.(css|js|png|jpg|webp|woff2|svg)(\?.*)?$/)) {
event.respondWith(
caches.match(event.request).then(function(cached) {
return cached || fetch(event.request).then(function(response) {
var clone = response.clone();
caches.open(STATIC_CACHE).then(function(cache) {
cache.put(event.request, clone);
});
return response;
});
})
);
return;
}
// Network-first for pages
event.respondWith(
fetch(event.request)
.then(function(response) {
var clone = response.clone();
caches.open(DYNAMIC_CACHE).then(function(cache) {
cache.put(event.request, clone);
});
return response;
})
.catch(function() {
return caches.match(event.request)
.then(function(cached) { return cached || caches.match('/offline.html'); });
})
);
});
The CACHE_VERSION string is the lever for cache busting. When CSS or JS changes, increment the version. The activate handler deletes all caches that don’t match the current version names, forcing a clean re-download on next load.
The service worker file itself must be served with Cache-Control: no-cache so the browser always checks for an updated version:
<Files "sw.js">
Header set Cache-Control "no-cache, no-store, must-revalidate"
</Files>
The cache strategy asymmetry matters: static assets (CSS, JS, fonts, images) almost never change between visits — cache-first is correct. Inventory pages change constantly — network-first is correct. Using the same strategy for both produces either stale inventory or slow static assets.
The delete-old-caches pattern in the activate handler is what prevents cache accumulation. Without it, every version increment leaves orphaned caches consuming device storage.
Service worker in production for 14 months. The CACHE_VERSION has been incremented 6 times — once per significant deployment. The offline page has been served to 340 unique visitors, primarily at boat shows and marinas with poor signal. Average page load time on repeat visits (returning within 24 hours): 0.8 seconds vs. 2.1 seconds for first visits, with static assets served from cache.
CACHE_VERSION on every deploy that changes CSS or JS. If you don’t, users continue to see the old version from cache. The version string is your deploy lever.sw.js with no-cache headers. If the browser caches the service worker file itself, version updates don’t propagate. The worker file must bypass HTTP caching.activate handler. Without deletion, every version increment accumulates another cache. Device storage is not unlimited.Every lesson stays free — no account, no paywall, no email gate, ever. But if you’d rather have this system standing on your business than wire all 48 lessons yourself, leave your email. We’ll send you a direct line to a build — and you’ll be first to hear when we add new tools to the curriculum.
None of this gates a single lesson. The curriculum was free before you got here and it stays that way.
You came here to understand the system, and now you do. If you’d rather have it standing on your business than spend the next three months wiring it yourself, GAP Concierge is the same architecture from these lessons — a white-label AI agent that knows your catalog and captures your leads — set up for you, from $97/mo.
See GAP Concierge →