Service Worker Cache โ Part 1: Specifying What to Cache ๐พ
Lecture 1: Specifying Cached URLs ๐๏ธ
Version the Cache Name, Not Just the SW
Bump to version 3 and tie the cache name to it:
const version = 3;
const cacheName = `ramblings-${version}`; // e.g. "ramblings-3"This one string controls everything. When version bumps to 4, the cache becomes ramblings-4 โ a completely new cache. The old one (ramblings-3) is left behind to be cleaned up during activation.
Two Cache Update Strategies
| Strategy | How | Trade-off |
|---|---|---|
| Atomic (Kyle's approach) | Version the cache name โ bump version = new cache for everything | Slightly more bandwidth on updates, but everything stays perfectly in sync |
| Incremental | Fixed cache name, update only changed resources | Less bandwidth, but more complex โ needs a server API to know what changed |
For this project: atomic. Every update re-caches everything under a new name. Clean, simple, predictable.
What to Cache โ urlsToCache
Instead of relying on the browser's opaque caching, we explicitly declare exactly what we want cached:
const urlsToCache = {
loggedOut: [
'/',
'/about',
'/contact',
'/login',
'/404',
'/offline',
'/css/style.css',
'/js/blog.js',
'/js/sw.js',
'/images/logo.png',
// ... all static assets
]
};Two things worth noting:
- The
loggedOutkey โ this is intentional. The structure allows for a second list (e.g.loggedIn) for session-sensitive resources. Kyle uses this in his own app to cache admin pages only for admin users โ no point pushing admin code to non-admins. - No
.htmlextensions โ the URLs match the actual routes the server responds to, not the file names on disk.
Why Hard-Code the List?
A few valid approaches here:
- Hard-coded array (what we're doing) โ simple, works for small sites
- Generated by build process โ many teams have their build tool dump all asset URLs directly into
sw.jsas a template. Automatic and always up to date. - Fetched from a server endpoint โ the SW hits an API to get the current list of resources to cache. More dynamic, more complex.
Pick what fits your project. The key is that you're in control of the list โ not the browser.
Next up: Caching Q&A โ questions about cache strategy before writing the actual caching code.
Lecture 2: Caching Q&A ๐
Short but two genuinely useful clarifications came out of this.
Can You Cache an Entire Folder?
Not directly โ the SW needs explicit URLs, not directory paths. But you have options:
- Hard-code the URLs โ simple, works for small known sets
- Hit a server API โ ask the server for a list of resource URLs, then generate the cache entries from that. Kyle mentions this is exactly what they'll do later for blog post URLs โ call an API to get post IDs, then construct the URLs from those IDs
- Generate programmatically โ any combination of the above
"The service worker needs to know what to request. It has to say, I want this. Part of the reason is that we need a unique URL to store as the key in the cache."
Each cached resource needs a URL โ that's its key in the cache store. No URL, no cache entry.
Why Do Some URLs Have Extensions and Others Don't?
This tripped a student up โ CSS and JS files have extensions (.css, .js) but HTML pages don't (/contact not /contact.html).
The answer is server-side URL routing:
- The server is configured to rewrite friendly URLs โ
/contactโcontact.html,/offlineโoffline.html - But it doesn't rewrite static asset paths โ
/css/style.cssis literally/css/style.csson disk
"The URLs you list here are the URLs as they would be loaded from the client to the server โ whatever that is. If your URLs have
.htmlon the end, put.htmlhere. It's not going to do any magic."
The rule: list URLs exactly as the browser would request them. Check your browser's address bar and network tab โ that's the URL to cache.
Next up: Adding to Service Worker Cache โ writing the actual code to fetch and store these URLs during install.
Lecture 3: Adding to Service Worker Cache ๐ฅ
The cacheLoggedOutFiles() Function
This is the core caching function โ fetches every URL in urlsToCache.loggedOut and stores it in the Cache API:
async function cacheLoggedOutFiles(forceReload = false) {
let cache = await caches.open(cacheName);
return Promise.all(
urlsToCache.loggedOut.map(async function requestFile(url) {
try {
let res;
if (!forceReload) {
res = await cache.match(url);
if (res) return; // already cached, skip
}
let fetchOptions = {
method: 'GET',
credentials: 'omit', // no cookies for logged-out resources
cache: 'no-cache' // always get fresh from server
};
res = await fetch(url, fetchOptions);
if (res.ok) {
await cache.put(url, res.clone()); // always clone!
}
} catch(err) {
// network might be down โ fail silently
}
})
);
}Breaking Down the Key Decisions
1. forceReload parameter (default false)
Two modes of operation:
- Normal (
forceReload = false) โ check the cache first. If the resource is already there, skip it. Only fetch what's missing. - Force (
forceReload = true) โ skip the cache check, always fetch fresh from the server. Used when you know a new version needs to replace the old one.
This handles the edge case where a previous caching run was interrupted (browser crashed, network dropped mid-install) and the cache is only partially filled.
2. credentials: 'omit'
These are logged-out resources โ they don't need cookies. Stripping credentials ensures you always get the public version of the response, not a session-specific one that might differ per user.
3. cache: 'no-cache'
This one feels counterintuitive โ you're caching things, but you're telling the browser not to cache. Here's why:
"We wanna tell the browser layer โ don't store this response in your intermediary cache, we want fresh results. If you don't do this, the browser may feed your service worker from its own cache, which defeats the whole purpose of having control over caching."
You want the response straight from the server, not a stale browser-cached copy. You're taking over caching responsibility โ the browser's intermediary cache is now in the way.
**4. res.clone() โ The Most Critical Detail โ ๏ธ
await cache.put(url, res.clone()); // never put the raw responseA response object can only be consumed once. If you cache it and also try to return it to the browser, one of them will fail with a cryptic "headers already closed" error.
The rule: always clone before putting into cache.
In this specific function it's not strictly necessary (we're not returning the response to anyone โ we fetched it ourselves), but Kyle does it anyway as a habit. When you're in a fetch handler intercepting real browser requests, forgetting this will cause hard-to-debug failures.
5. Wrap in try/catch
The entire fetch is wrapped in a try/catch because the network might be down during installation. If a resource fails to cache, it fails silently โ no crash, no broken install. The SW continues caching everything else it can reach.
The Cache API Flow
caches.open(cacheName) โ get a reference to our named cache
cache.match(url) โ check if URL is already cached
fetch(url, fetchOptions) โ get fresh copy from server
cache.put(url, res.clone()) โ store cloned response in cacheNext up: Service Worker Cache Demo โ seeing the cache populate in DevTools and testing offline behaviour.
Lecture 4: Service Worker Cache Demo ๐ฌ
Where to Call cacheLoggedOutFiles()
The function exists but needs to be called in two places:
1. In main() โ every time the SW starts:
async function main() {
console.log(`Service Worker (v${version}) is starting`);
await sendMessage({ requestStatusUpdate: true });
await cacheLoggedOutFiles(); // forceReload = false (default)
}No force reload here โ just a good faith check. If anything is missing from the cache, fetch it. If it's already there, skip it.
2. In handleActivation() โ on first activation:
async function handleActivation() {
await clients.claim();
await cacheLoggedOutFiles(/* forceReload= */ true); // always fetch fresh
console.log(`Service Worker (v${version}) activated`);
}Force reload here โ this is a new version activating, so unconditionally fetch everything fresh from the server regardless of what's already in the cache.
Kyle's stylistic habit: always add an inline comment when passing a boolean argument โ
/* forceReload= */ trueโ so the reader doesn't have to look up the function signature to understand whattruemeans.
Two Common Bugs Caught Live ๐
Kyle hit two real bugs during the demo โ both worth knowing:
Bug 1: caches.match instead of caches.open
// โ Wrong โ match looks for a cached response, doesn't open a cache store
let cache = await caches.match(cacheName);
// โ
Correct โ open gets a reference to the named cache store
let cache = await caches.open(cacheName);caches.match searches for a cached response by URL. caches.open opens (or creates) a named cache store. Easy to confuse, hard to spot.
Bug 2: Typo in the CSS filename
// โ Wrong
'/css/styles.css'
// โ
Correct
'/css/style.css'The cache silently skips files that 404 โ no error thrown. You just end up with a missing cache entry and spend time wondering why.
The Safe Way to Force a Fresh Install in DevTools
Kyle's recommended sequence when you need to force the SW to re-run its install from scratch:
- Stop the service worker first (Application tab โ Stop)
- Unregister it (Application tab โ Unregister)
- Navigate to the page โ a fresh SW will install from zero
"I always stop first, then unregister. I've found weirdnesses and quirks with just clicking Update or Unregister directly while it's still running."
Verifying the Cache in DevTools โ
After a clean install, the Application tab โ Cache Storage shows:
ramblings-3โ the named cache entry- Every URL listed inside it
- Click any entry to preview the response body or inspect headers
- Right-click to delete individual entries (useful for targeted debugging)
- Right-click the cache name to delete the entire cache
"This is a good way of verifying that we've loaded up all the stuff into the cache. That's a big step for being able to go offline."
What This Enables
With all static assets now in the cache, the SW has everything it needs to intercept requests and serve them offline. The wiring is in place โ the fetch handler comes next.
Next up: Clearing Service Worker Cache โ cleaning up old versioned caches during activation so stale data doesn't accumulate.
Lecture 5: Clearing Service Worker Cache ๐งน
Why You Must Clean Up Old Caches
Every time the version bumps, a new cache (ramblings-4, ramblings-5, etc.) is created. The old one doesn't disappear automatically. Left unchecked, you'll eventually hit the browser's storage quota โ at which point nothing can be cached at all.
Caches have no automatic expiration. They persist until either the developer deletes them or the user manually clears storage in DevTools. A regular browser cache clear or shift-reload does not touch Cache API storage.
When to Clear โ Activation, Not Install โ ๏ธ
This is an important timing decision:
โ Don't clear during install โ the old SW might still be using those caches
โ
Clear during activation โ the old SW is guaranteed to be deactivatedDeleting caches while the old service worker is still running could crash its in-flight operations. Waiting until activation means you have exclusive ownership.
The clearCaches() Function
async function clearCaches() {
let cacheNames = await caches.keys();
let oldCacheNames = cacheNames.filter(function(cacheName) {
// Only target caches that match our naming convention
if (/^ramblings-\d+$/.test(cacheName)) {
let cacheVersion = Number(cacheName.match(/\d+$/)[0]);
return (cacheVersion > 0 && cacheVersion !== version);
}
return false; // not our cache, leave it alone
});
return Promise.all(
oldCacheNames.map(function(cacheName) {
return caches.delete(cacheName);
})
);
}Three key decisions:
caches.keys()โ gets all cache names across the entire origin, not just yours- Regex filter โ only target caches matching
ramblings-{number}. Third-party scripts on the same site may have created their own caches โ don't touch those - Version number check โ keep the current version, delete everything else. Guard against non-numeric matches just to be safe
Updated handleActivation() โ Order Matters
async function handleActivation() {
await clients.claim();
await clearCaches(); // 1. delete old caches first
await cacheLoggedOutFiles(/* forceReload= */ true); // 2. prime new cache
console.log(`Service Worker (v${version}) activated`);
}Clear old caches first, then populate the new one. Clean slate before fresh start.
Seeing It Work โ
After bumping to version 4 and navigating the page:
ramblings-3disappears from the Application tabramblings-4appears with all the newly cached resources
Cache Expiration โ The Developer's Responsibility
A Q&A moment worth keeping:
"Will the cache eventually expire?"
"No, there is absolutely no expiration on these caches."
The Cache API has no TTL. It's entirely on you to decide when content should be refreshed. Two practical approaches:
- Version bumps โ every SW update clears and re-primes the cache. Natural expiration tied to your deploy cycle.
- Timed updates โ push a new SW version on a schedule (e.g. every two weeks) to force a cache refresh even if the logic hasn't changed.
Full Section Recap ๐ฏ
Across all five lectures in this section:
- Versioned cache names โ
ramblings-${version}ties cache lifetime to SW version, enabling atomic updates urlsToCacheโ explicit URL lists, organized by auth state, with support for dynamic generationcacheLoggedOutFiles()โ opens the cache, checks before fetching, usesno-cacheheaders for fresh responses, always clones before storing- Where to call it โ
main()without force (fill gaps),handleActivation()with force (always fresh) clearCaches()โ regex-filtered deletion of old versioned caches, only at activation time, never during install