GAP School Module 09 — SMS, Mobile, PWA Lesson 9.2

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 situation

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.


What I did

Service worker registration

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:

JavaScript
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'); } }); }); }); }); }

Cache strategy by content type

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:

JavaScript — sw.js
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'); }); }) ); });

Cache versioning and sw.js headers

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:

Apache / .htaccess
<Files "sw.js"> Header set Cache-Control "no-cache, no-store, must-revalidate" </Files>

Why it matters

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.


The Anchor build

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.


Do this, not that

  • Increment 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.
  • Serve 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.
  • Delete old caches in the activate handler. Without deletion, every version increment accumulates another cache. Device storage is not unlimited.
  • Don’t intercept WP admin or API requests. The service worker should be transparent to the CMS backend. Admin interactions that hit a stale cache are a debugging nightmare.
  • Build the offline page in plain HTML. The offline page is served from cache when there’s no network. It can’t depend on external resources. Keep it self-contained.
When you’re ready to build

The lessons are yours. When you want it built, we’re here.

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.

We’ll use your email to send you a fast-track to a GAP build and occasional notes on how GAP builds digital sales departments. Lessons stay 100% free — no email required to read any of them. We never share or sell your information. Unsubscribe any time. Privacy policy at gapindustriesllc.com/privacy.html.

Done learning how it’s built? We’ll build it.

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 →