Lesson 0003 · ~12 minutes

Strategies & service workers

One skill: pair Cache Storage with a service worker and pick a caching strategy that matches freshness vs offline needs.

Win for this lesson

In a design review you can say: “warehouse vs policy,” name install/activate jobs, and pick cache-first vs network-first vs stale-while-revalidate for a given resource — without building a full PWA checklist yet.

1. Warehouse vs policy

Cache Storage stores Request/Response pairs. It does nothing automatic on page load.

A service worker can intercept fetch and call event.respondWith(...). That policy is the strategy. Jake Archibald’s Offline Cookbook is the classic catalog: caching and serving are separate knobs — use them in tandem by URL and context.

High-level browser order when a resource is requested (SW caching and HTTP caching):

  1. Service worker (if controlling and you handle fetch)
  2. HTTP cache (headers / browser)
  3. Network (CDN / origin)
Default rule (steal this)

Cache Storage = where HTTP-shaped resources live. The service worker = when and how they are filled and served. Different routes can use different strategies. Workbox is optional sugar later — the decisions are the same.

2. When to fill the warehouse

From the Offline Cookbook’s “when to store” map (not every product needs all of these):

install

Precache versioned shell assets with event.waitUntil(caches.open(...).then(c => c.addAll(...))). If addAll fails, install fails — good for must-have deps.

activate

Delete old named caches once the previous SW is gone. Keep activation lean — long activate queues fetch and blocks loads.

fetch / runtime

On network response: if no cache hit, fetch, return, and put a clone for next time. Watch storage bloat (images, unbounded URLs).

user action

“Save offline” from the page: Cache API works in window scope too — open a named cache and addAll what the user chose.

// install: must-have static for this SW version
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open("static-v3").then((cache) =>
      cache.addAll(["/index.html", "/app.js", "/app.css"])
    )
  );
});

// activate: drop previous generations (careful: origin-wide names)
self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((names) =>
      Promise.all(
        names
          .filter((n) => n.startsWith("static-") && n !== "static-v3")
          .map((n) => caches.delete(n))
      )
    )
  );
});

3. How to serve (strategies that matter first)

Names vary slightly across docs; meanings below match the Offline Cookbook and the strategy table on web.dev.

Strategy Idea Classic fits
Cache, falling back to network match → else fetch Offline-first shell; most static for a versioned build
Network, falling back to cache fetch → on fail, match Prefer fresh (status, prices with caveats); stale if offline
Stale-while-revalidate Return cache if any; also fetch and put for next time Avatars, product lists, “fast now, fresher later”
Network only Always network (or skip respondWith) Payments, analytics pings, non-GET
Cache only Only match (miss ≈ error) Precached assets you know exist for this version
// Cache, falling back to network
self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request);
    })
  );
});

// Network, falling back to cache
self.addEventListener("fetch", (event) => {
  event.respondWith(
    fetch(event.request).catch(() => caches.match(event.request))
  );
});

// Stale-while-revalidate (core idea)
self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.open("dyn-v1").then(async (cache) => {
      const cached = await cache.match(event.request);
      const networkPromise = fetch(event.request).then((res) => {
        cache.put(event.request, res.clone());
        return res;
      });
      return cached || networkPromise;
    })
  );
});
HTTP cache can “fight” revalidate

When the SW goes to the network, the browser may still serve a long max-age from the HTTP cache. Strategy + header design should cooperate; SW TTL and HTTP TTL need not be identical — web.dev often suggests longer SW-side control when you want offline reliability (same article). Deep header recipes can wait; know the two layers exist.

4. Practice — pick the strategy

Equal-length options. Immediate feedback.

Scenario A

Fingerprinted /app.abc123.js and CSS that must load offline and instantly on repeat visits.

Scenario B

Checkout total and payment intent must never show a stale cached amount as if it were live.

Scenario C

Order status: prefer fresh when online; if the network fails, last cached status is acceptable with a disclaimer.

Scenario D

User avatars: show something immediately; latest image on a later visit is fine.

Scenario E

You shipped static-v4. Where do you usually delete static-v3 named caches?

5. What to remember

Ask your teacher Anything fuzzy — Workbox vs hand-rolled, navigation preload, or a route on your team — ask in chat.

Primary source (read next)

web.dev — Offline Cookbook (Jake Archibald). Best primary for “when to store” + serving patterns. Optional second: Service worker caching and HTTP caching for the two-layer model.