Chapter 85: Progressive Web Apps (PWAs)

PWA Caching Strategies

Engineering deterministic network routing policies: Cache-First, Network-First, Stale-While-Revalidate, Network-Only, and Cache-Only.

LEARNING OBJECTIVES
  • Understand how the Service Worker fetch event intercepts and proxies HTTP traffic between the browser and network.
  • Master the 5 fundamental caching patterns: Cache-First, Network-First, Stale-While-Revalidate, Network-Only, and Cache-Only.
  • Implement stream cloning mechanics (response.clone()) to safely satisfy both the browser rendering engine and CacheStorage.
  • Build a multi-strategy dynamic router that matches request types (HTML, JS/CSS bundles, API JSON, static images) to their optimal caching strategy.
🎬 INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
🌐
1. Input
Directives & Tags
⚙️
2. Parse
Tokenizer & AST
🌳
3. Layout
Box Model & Flow
🎨
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

📖 The Mental Model & Story (Intuitive Foundation)

Imagine a high-end chef working in a busy restaurant kitchen. When an order arrives:

  • For salt, pepper, and flour (static CSS/JS assets that never change), the chef immediately reaches into the kitchen pantry (Cache-First). They do not call the distributor every time a pinch of salt is needed.
  • For live lobster or fresh fish (account balances and critical financial APIs), the chef calls the harbor immediately (Network-First). If the harbor is closed or the line is dead, only then do they check the freezer as an emergency fallback.
  • For daily newspaper or daily specials board (avatars, articles, social feeds), the chef quickly reads yesterday's board to the customer while sending an apprentice out the back door to fetch the latest edition (Stale-While-Revalidate).
  • For credit card payment processing (POST checkout mutations), the chef only communicates through the bank terminal and never attempts to cache the transaction (Network-Only).

In a Progressive Web App, your Service Worker is the chef. It routes every inbound network request to the appropriate storage strategy based on the data's freshness requirements and performance profile.


Technical Deep Dive & Specifications

The 5 Core Caching Strategies

+----------------------------------------------------------------------------------------------------+
| 1. CACHE-FIRST (Cache Falling Back to Network)                                                     |
| Best for: Hashed static assets (bundle.8a7f.js, styles.css, fonts, webp images)                   |
| Request ===> Check Cache? ===[Hit]===> Return Cached Response                                      |
|                     |                                                                              |
|                  [Miss] ===> Fetch from Network ===> Put into Cache ===> Return Network Response   |
+----------------------------------------------------------------------------------------------------+

+----------------------------------------------------------------------------------------------------+
| 2. NETWORK-FIRST (Network Falling Back to Cache)                                                   |
| Best for: Frequently updated data (user inbox, real-time analytics, dynamic HTML)                  |
| Request ===> Fetch Network ===[Success]===> Update Cache ===> Return Network Response             |
|                     |                                                                              |
|                  [Failure/Offline] ===> Return Cached Response (Fallback)                          |
+----------------------------------------------------------------------------------------------------+

+----------------------------------------------------------------------------------------------------+
| 3. STALE-WHILE-REVALIDATE (Instant Cache + Background Network Refresh)                              |
| Best for: Non-critical dynamic content (avatars, news lists, category feeds)                       |
| Request ===> Return Cached Response IMMEDIATELY (Fastest UX)                                       |
|                     +======> [Background Thread] Fetch Network ===> Update Cache for Next Visit    |
+----------------------------------------------------------------------------------------------------+

+----------------------------------------------------------------------------------------------------+
| 4. NETWORK-ONLY                                        | 5. CACHE-ONLY                             |
| Best for: Non-idempotent POST/PUT, payment APIs        | Best for: Fixed App Shell & offline page  |
| Request ===> Network (Bypasses Cache entirely)        | Request ===> CacheStorage (Never touches  |
|                                                        |               network)                    |
+----------------------------------------------------------------------------------------------------+

Comparison Matrix

Strategy Performance / Latency Freshness Offline Support Recommended Use Cases
Cache-First ⚡ Instant (0–10ms) Low (until invalidated) 🟢 100% Web fonts, versioned JS/CSS, brand logos, static SVG icons.
Network-First ⏳ Network bound (100–1000ms) 🟢 Real-time 🟡 Fallback only HTML navigation documents, user balance APIs, stock tickers.
Stale-While-Revalidate ⚡ Instant (0–10ms) 🟡 Near real-time 🟢 100% User profiles, article feeds, product catalogs, dashboard metrics.
Network-Only ⏳ Network bound 🟢 Real-time 🔴 None (fails offline) Checkout payments, authentication tokens, live chat sockets.
Cache-Only ⚡ Instant (0–5ms) Fixed at install 🟢 100% Offline fallback HTML, isolated embedded offline help docs.

The Response Stream Cloning Rule (response.clone())

The Fetch API Response object is a single-use readable stream (ReadableStream). Once the browser consumes the stream to paint the UI or parse JSON, the stream is locked and cannot be read a second time.

                      +-----------------------------+
                      |   Network Fetch Response    |
                      +-----------------------------+
                                     |
                          [ Call response.clone() ]
                                     |
                   +-----------------+-----------------+
                   |                                   |
                   v                                   v
        +---------------------+             +---------------------+
        | Stream 1: Original  |             |  Stream 2: Clone    |
        | Return to Browser   |             | Put into Cache      |
        | window for DOM      |             | storage bucket      |
        +---------------------+             +---------------------+
// MUST clone response before putting it into cache!
const networkResponse = await fetch(event.request);
const clonedResponse = networkResponse.clone();
await cache.put(event.request, clonedResponse);
return networkResponse;

💻 Interactive Code Playground

Starter Code: Production Multi-Strategy Router

Below is a complete, modular Service Worker demonstrating dynamic strategy routing based on request destination and URL patterns.

Line-by-Line Code Breakdown

  • Line 37 (request.destination): Uses standard Fetch metadata ('style', 'script', 'font', 'image') to cleanly route static resources without brittle regex file extensions.
  • Line 43 (url.pathname.startsWith('/api/feed')): Routes API read feeds through Stale-While-Revalidate for instantaneous UI response.
  • Line 49 (request.mode === 'navigate'): Identifies browser address bar URL changes and page reloads to execute Network-First with App Shell fallback.
  • Line 66 (cache.put(request, networkResponse.clone())): Clones the response stream before persisting it in CacheStorage, allowing the original stream to be returned to the DOM.
  • Line 104 (return cachedResponse || networkFetch): Implements the Stale-While-Revalidate magic: returns cached bytes immediately while the background promise networkFetch updates storage concurrently.

Expected Browser Render Output


// sw.js - Production Multi-Strategy Routing Engine
const STATIC_CACHE = 'static-assets-v1';
const DYNAMIC_CACHE = 'dynamic-content-v1';

// 1. Install & Precache core assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(STATIC_CACHE).then((cache) => {
      return cache.addAll([
        '/',
        '/index.html',
        '/styles/main.css',
        '/scripts/app.js'
      ]);
    })
  );
  self.skipWaiting();
});

// 2. Activate & Purge
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) => {
      return Promise.all(
        keys.map((key) => {
          if (key !== STATIC_CACHE && key !== DYNAMIC_CACHE) {
            return caches.delete(key);
          }
        })
      );
    }).then(() => self.clients.claim())
  );
});

// 3. Dynamic Strategy Router
self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);

  // Strategy A: Cache-First for static assets (CSS, JS, Fonts, Images)
  if (['style', 'script', 'font', 'image'].includes(request.destination)) {
    event.respondWith(cacheFirstStrategy(request));
    return;
  }

  // Strategy B: Stale-While-Revalidate for API Feed requests
  if (url.pathname.startsWith('/api/feed')) {
    event.respondWith(staleWhileRevalidateStrategy(request));
    return;
  }

  // Strategy C: Network-First for HTML navigation and user profiles
  if (request.mode === 'navigate' || url.pathname.startsWith('/api/user')) {
    event.respondWith(networkFirstStrategy(request));
    return;
  }

  // Default: Network Only
  event.respondWith(fetch(request));
});

// Strategy Implementation: CACHE-FIRST
async function cacheFirstStrategy(request) {
  const cachedResponse = await caches.match(request);
  if (cachedResponse) {
    return cachedResponse;
  }
  try {
    const networkResponse = await fetch(request);
    if (networkResponse.status === 200) {
      const cache = await caches.open(STATIC_CACHE);
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  } catch (err) {
    return new Response('Asset unavailable offline', { status: 408 });
  }
}

// Strategy Implementation: NETWORK-FIRST
async function networkFirstStrategy(request) {
  try {
    const networkResponse = await fetch(request);
    if (networkResponse.status === 200) {
      const cache = await caches.open(DYNAMIC_CACHE);
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  } catch (err) {
    const cachedResponse = await caches.match(request);
    if (cachedResponse) {
      return cachedResponse;
    }
    // Fallback for HTML navigations
    if (request.mode === 'navigate') {
      return caches.match('/index.html');
    }
    return new Response(JSON.stringify({ error: 'Offline', offline: true }), {
      headers: { 'Content-Type': 'application/json' },
      status: 503
    });
  }
}

// Strategy Implementation: STALE-WHILE-REVALIDATE
async function staleWhileRevalidateStrategy(request) {
  const cache = await caches.open(DYNAMIC_CACHE);
  const cachedResponse = await cache.match(request);

  // Background fetch to refresh the cache
  const networkFetch = fetch(request).then((networkResponse) => {
    if (networkResponse.status === 200) {
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  }).catch(() => {
    // Network failed, silent catch for background revalidation
  });

  // Return cached version immediately if present, otherwise await network
  return cachedResponse || networkFetch;
}
(First Page Load - Online):
[Network] GET /styles/main.css -> 200 OK (Downloaded & Cached in static-assets-v1)
[Network] GET /api/feed -> 200 OK (Downloaded & Cached in dynamic-content-v1)

(Second Page Load - Instantaneous):
[SW] GET /styles/main.css -> (Served from ServiceWorker Cache: 2ms)
[SW] GET /api/feed -> (Served from ServiceWorker Cache: 3ms)
[SW Background] GET /api/feed -> 200 OK (Cache updated silently for next visit)

(Third Page Load - Airplane Mode / Offline):
[SW] GET /index.html -> (Network failed -> Fallback to cached index.html)
[SW] GET /styles/main.css -> (Served from ServiceWorker Cache)

🏋️ Hands-On Exercise

🎯 The Challenge: Build a Stale-While-Revalidate Image Cache

Instructions:

  1. Intercept any fetch event whose URL ends in .jpg, .png, or .webp.
  2. Open a cache bucket named 'image-store-v1'.
  3. Check the cache: if an image exists, return it immediately.
  4. Concurrently fire a network fetch(), update the cache with the cloned response upon success, and ensure no unhandled promise rejections occur if the network is down.

🏁 Starter Code Sandbox

⚠️ Common Pitfalls

  1. Caching POST, PUT, or DELETE Requests in CacheStorage: The CacheStorage API only supports GET requests according to the W3C specification. Calling cache.put() with a POST request throws an unhandled TypeError: Request method 'POST' is unsupported.
  2. Forgetting to Clone Responses: Calling cache.put(request, response) without calling response.clone() consumes the body stream, causing event.respondWith(response) to fail with TypeError: Already read.
  3. Caching Opaque Cross-Origin Responses blindly: When fetching non-CORS cross-origin resources (mode: 'no-cors'), the browser returns an opaque response (status 0). Opaque responses consume a massive padded quota (often 7MB+ per response in Chromium) regardless of true file size.

💡 Pro Tips

  1. Apply Network Timeouts to Network-First: On high-latency or unstable mobile connections ("Lie-Fi"), a standard fetch() can take 30–60 seconds before failing. Wrap your fetch() in a Promise.race() with a 3-second timeout that falls back to cache if the server does not respond quickly.
  2. Enforce Cache Limits via LRU Eviction: Unchecked dynamic caches will grow indefinitely. Implement a Least-Recently-Used (LRU) cleanup helper that deletes the oldest entries when a cache bucket exceeds a threshold (e.g., 50 items).

📌 Key Takeaways

  • The fetch event turns the Service Worker into a client-side programmable network proxy.
  • Cache-First provides the fastest possible load times for immutable, hashed static assets.
  • Network-First guarantees fresh content while providing an offline safety net for essential data.
  • Stale-While-Revalidate provides zero-latency responses while refreshing storage asynchronously in the background.
  • Response streams are single-use; you must call response.clone() before writing to CacheStorage.
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

Which caching strategy delivers instantaneous zero-latency responses to the UI while asynchronously fetching a fresh copy from the network in the background?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

What happens if you attempt to store a POST HTTP request in the CacheStorage API using cache.put()?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

Why is it mandatory to call response.clone() before passing a fetch response to cache.put()?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP