Chapter 85: Progressive Web Apps (PWAs)

Building an Offline Experience & Fallbacks

Engineering the App Shell architecture, custom offline fallback pages, precaching critical UI assets, and seamless offline state transitions.

LEARNING OBJECTIVES
  • Architect a resilient App Shell Architecture that separates the static application frame from dynamic data streams.
  • Implement precaching of critical HTML, CSS, JavaScript, and SVG assets during the Service Worker install phase.
  • Route failed HTML page navigations to a branded, high-utility offline.html fallback document.
  • Build client-side offline detection banners with automatic reconnection notifications.
🎬 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 boarding a commercial flight with a native e-reader device (like a Kindle). Even though your airplane has zero Wi-Fi connectivity at 35,000 feet, the Kindle hardware still boots instantly. The user interface buttons (the library menu, chapter picker, font settings) are physical components etched into the device firmware—they do not require a network connection to exist. The device simply displays whatever books were previously synced to local storage.

Now imagine a traditional web page. When the network vanishes, the entire browser window collapses into the browser's generic "Downasaur / No Internet" screen.

The App Shell Architecture bridges this gap. The "shell" is the minimal HTML, CSS, and JavaScript required to power the visual chrome and navigation frame of your application. By precaching the App Shell, your web application boots in 50 milliseconds in the middle of a desert, rendering its full navigation headers, tabs, and skeleton loaders while gracefully notifying the user that live server updates will resume once connectivity returns.


Technical Deep Dive & Specifications

The App Shell Architecture

The App Shell separates static infrastructure from dynamic content:

+-----------------------------------------------------------------------------------+
|                              APP SHELL (PRECACHED)                                |
|  +-----------------------------------------------------------------------------+  |
|  | Header: [ App Logo ]                       [ Notifications ] [ Profile Pic ] |  |
|  +-----------------------------------------------------------------------------+  |
|  | Navigation: [ Home ]   [ Explore ]   [ Library ]   [ Settings ]             |  |
|  +-----------------------------------------------------------------------------+  |
|                                                                                   |
|  +-----------------------------------------------------------------------------+  |
|  |                        DYNAMIC CONTENT VIEWPORT                             |  |
|  |                                                                             |  |
|  |  [ ONLINE ]: Fetches live JSON and populates reactive templates             |  |
|  |                                                                             |  |
|  |  [ OFFLINE ]: Reads local IndexedDB cache or displays offline fallback UI   |  |
|  |                                                                             |  |
|  +-----------------------------------------------------------------------------+  |
+-----------------------------------------------------------------------------------+
  1. The Shell: index.html, global.css, app.js, brand icons. Precached on worker install.
  2. The Content: Dynamic API payloads, user messages, article bodies. Cached dynamically at runtime.

Navigation Fallback Mechanics

When a user clicks a link to an uncached page (e.g., https://example.com/articles/deep-dive-into-wasm) while offline:

  1. The browser initiates a navigation request (request.mode === 'navigate').
  2. The Service Worker attempts a network fetch.
  3. The network fetch rejects (TypeError: Failed to fetch).
  4. The Service Worker catches the rejection and returns caches.match('/offline.html').
                    +--------------------------------+
                    | User Navigates to /article/99  |
                    | (request.mode === 'navigate')  |
                    +--------------------------------+
                                   |
                                   v
                    +--------------------------------+
                    |  Service Worker Interception   |
                    +--------------------------------+
                                   |
                                   v
                    +--------------------------------+
                    |     Attempt Network Fetch      |
                    +--------------------------------+
                                   |
                    +--------------+--------------+
                    | (Network OK)                | (Network Fails / Offline)
                    v                             v
        +-----------------------+     +-----------------------+
        | Return 200 Live Page  |     | Catch Rejection &     |
        | Update dynamic cache  |     | Return /offline.html  |
        +-----------------------+     +-----------------------+

💻 Interactive Code Playground

Starter Code: Complete App Shell + Offline Fallback System

1. File: offline.html (Dedicated Offline Branded Fallback)

2. File: sw.js (App Shell Precaching & Navigation Routing)

3. File: index.html (App Shell Host with Live Network Toast)

Line-by-Line Code Breakdown

  • sw.js Line 3 (PRECACHE_MANIFEST): Explicitly enumerates the exact file set required to render the application's core visual framework and offline message.
  • sw.js Line 38 (if (request.mode === 'navigate')): Detects document-level URL changes in the browser address bar, ensuring that sub-resource fetch failures do not trigger full offline page replacements.
  • sw.js Line 46 (return caches.match('/offline.html')): The guaranteed offline fallback guarantee; if both the live network and the requested cached document miss, the branded offline UI renders.
  • index.html Line 47–59 (window.addEventListener('offline' | 'online')): Coordinates immediate visual feedback in the UI thread when hardware connectivity changes.

Expected Browser Render Output


SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...
const SHELL_CACHE = 'app-shell-v1';
const PRECACHE_MANIFEST = [
  '/',
  '/index.html',
  '/offline.html',
  '/styles/app.css',
  '/scripts/app.js',
  '/icons/logo.svg'
];

// Precache App Shell
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(SHELL_CACHE).then((cache) => {
      console.log('[SW] Precaching complete App Shell & offline fallback.');
      return cache.addAll(PRECACHE_MANIFEST);
    })
  );
  self.skipWaiting();
});

// Purge Old Shells
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) => {
      return Promise.all(
        keys.map((key) => {
          if (key !== SHELL_CACHE) {
            return caches.delete(key);
          }
        })
      );
    }).then(() => self.clients.claim())
  );
});

// Intercept Navigations and Assets
self.addEventListener('fetch', (event) => {
  const { request } = event;

  // Case 1: HTML Page Navigation
  if (request.mode === 'navigate') {
    event.respondWith(
      fetch(request)
        .catch(async () => {
          // If offline, check if the specific page is cached, otherwise serve offline.html
          const cachedPage = await caches.match(request);
          if (cachedPage) {
            return cachedPage;
          }
          return caches.match('/offline.html');
        })
    );
    return;
  }

  // Case 2: Static App Shell Assets
  event.respondWith(
    caches.match(request).then((cachedResponse) => {
      return cachedResponse || fetch(request);
    })
  );
});
(User visits /news/today while online):
[Online View]: Full live news article loads normally.

(User switches to Airplane Mode and refreshes):
[Offline View]: App Shell loads instantly, offline toast appears:
[ ⚠️ No Internet Connection. Working Offline. ]

(User clicks a link to an uncached page /deep-analysis while offline):
[Offline Fallback]: 
+---------------------------------------------+
|                     📡                      |
|          You Are Currently Offline          |
| The page you requested is not saved in your |
| offline cache. Check connection and retry.  |
|            [ Retry Connection ]             |
+---------------------------------------------+

🏋️ Hands-On Exercise

🎯 The Challenge: Build an Offline Image & Document Fallback Router

Instructions:

  1. In a Service Worker fetch handler, identify requests for missing images (request.destination === 'image').
  2. If the network request fails and the image is not in cache, return a cached SVG placeholder (/images/offline-placeholder.svg).
  3. For navigation requests (request.mode === 'navigate'), return /offline.html upon network failure.
  4. For all other static asset requests, use Cache-First.

🏁 Starter Code Sandbox

⚠️ Common Pitfalls

  1. Serving offline.html for API Requests: If you use a broad fetch().catch(() => caches.match('/offline.html')) for all requests, an offline JSON API fetch (fetch('/api/tasks')) will receive an HTML string, causing response.json() to crash with SyntaxError: Unexpected token < in JSON at position 0.
  2. Precaching Large Media in the App Shell: Adding 20MB video files or high-res hero galleries to your PRECACHE_MANIFEST will cause the install phase to stall on slow 3G networks. Only precache the bare minimum visual shell.
  3. Failing to Precache offline.html: If offline.html is not in your precache manifest, attempting to serve it during an offline fallback will return undefined, resulting in the standard browser network error.

💡 Pro Tips

  1. Inject Dynamic Offline Content via IndexedDB: Instead of showing a static dead-end on offline.html, write a client-side script in offline.html that reads from IndexedDB and displays a list of articles or records the user did previously cache for offline reading.
  2. Use Skeleton Screens in the App Shell: Design your cached index.html shell with CSS animated gray skeleton cards. When the user opens the PWA offline, the UI renders the familiar layout instantly, reducing perceived load time to near zero.

📌 Key Takeaways

  • The App Shell Architecture separates the persistent UI frame (HTML/CSS/JS) from dynamic data payloads.
  • Precaching during the Service Worker install event guarantees that the App Shell is immediately available offline.
  • Navigation requests (request.mode === 'navigate') should fall back to a branded offline.html document when network requests fail.
  • Media requests (request.destination === 'image') should fall back to lightweight vector SVG placeholders.
  • Differentiate request types carefully to avoid returning HTML fallback documents to JSON API consumers.
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

Why must a Service Worker check request.mode === 'navigate' before returning an offline.html fallback response?

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

What is the core philosophy behind the App Shell Architecture?

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

If a critical asset listed in cache.addAll(PRECACHE_MANIFEST) returns a 404 Not Found error during the install event, what happens to the Service Worker?

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