DEV Community

Timevolt
Timevolt

Posted on

Building PWAs: Offline‑First Adventures — A **Back to the Future** Style Journey

The Quest Begins (The "Why")

Honestly, I used to think progressive web apps were just a fancy buzzword slapped onto any site that had a manifest file. I’d throw a manifest.json on a project, call it a day, and then watch users stare at a blank screen the moment their Wi‑Fi dropped. It felt like showing up to a party with no snacks—everyone’s excited until the music stops and you’re left staring at the void.

The turning point came when a friend complained that her favorite recipe site turned into a dinosaur every time she tried to cook while her phone was on the subway. “I just want to see the ingredients without pulling up a signal!” she exclaimed. That hit me: offline‑first isn’t a nice‑to‑have; it’s a core expectation. I realized I needed to slay the dragon of unreliable networks and give users a seamless experience, no matter where they are.

The Revelation (The Insight)

The treasure I uncovered was the service worker—a script that runs in the background, intercepts network requests, and lets us decide what to serve from cache or the network. Think of it as the DeLorean of web tech: with a little flux capacitor (a.k.a. the Cache API), you can travel back to a previously cached version of your site whenever the present network is missing.

The magic happens in three steps:

  1. Install – pre‑cache essential assets (HTML, CSS, JS, images).
  2. Activate – clean up old caches so you don’t end up with a garage full of rusty DeLoreans.
  3. Fetch – respond to requests with cached copies when offline, or go to the network when available, updating the cache as you go.

Once I saw how a few lines could turn a fragile site into a resilient time‑machine, I was hooked.

Wielding the Power (Code & Examples)

Before: The Fragile Site

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>My Awesome PWA</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <h1>Welcome!</h1>
  <p>This content disappears the moment you lose connection.</p>
  <script src="/app.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

If you yank the ethernet cord, the browser shows the dreaded “No internet” page—no fallback, no love.

After: Adding a Service Worker

First, register the worker from your main JS file:

// app.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(reg => console.log('Service worker registered 🚀', reg))
      .catch(err => console.error('Registration failed: ', err));
  });
}
Enter fullscreen mode Exit fullscreen mode

Now the real spell: /sw.js.

// sw.js
const CACHE_NAME = 'my-pwa-v1';
const ASSETS_TO_CACHE = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
  '/offline.png'   // a friendly image shown when truly offline
];

// Install – cache the core assets
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(ASSETS_TO_CACHE))
      .then(() => self.skipWaiting())
  );
});

// Activate – wipe old caches
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys()
      .then(keys => Promise.all(
        keys.filter(key => key !== CACHE_NAME)
            .map(key => caches.delete(key))
      ))
      .then(() => self.clients.claim())
  );
});

// Fetch – network‑first with fallback to cache
self.addEventListener('fetch', event => {
  // Skip cross‑origin requests (like analytics) to avoid CORS headaches
  if (!event.request.url.startsWith(self.location.origin)) return;

  event.respondWith(
    fetch(event.request)
      .then(response => {
        // Clone because response is a stream; we need one for cache, one for client
        const clone = response.clone();
        caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
        return response;
      })
      .catch(() => caches.match(event.request).then(cached => cached || caches.match('/offline.png')))
  );
});
Enter fullscreen mode Exit fullscreen mode

What changed?

  • Install caches the essential files the first time the user visits.
  • Activate ensures we don’t keep stale versions lying around—no more “I swear I updated that CSS!” moments.
  • Fetch tries the network first; if it fails, we serve the cached copy (or a cute offline image).

Common Traps (The “Bosses” to Avoid)

  1. Forgot to bump the cache version – If you change CACHE_NAME but never update it, the service worker will keep serving the old assets forever. Treat the version number like a game save point; increment it whenever you change something critical.

  2. Caching everything indiscriminately – Caching large videos or third‑party scripts can blow up storage quotas and slow down the install step. Be selective: cache only what you need for the core UI and maybe a fallback image.

  3. Not handling skipWaiting and clients.claim – Without these, the new worker might not take control of existing pages until a refresh, leaving users stuck on the old version.

When I finally nailed the flow, the site loaded instantly even when I turned off Wi‑Fi on my laptop—felt like hitting the “Back to the Future” button and watching the DeLorean reappear in my driveway.

Why This New Power Matters

Now you can build experiences that don’t betray the user when the network gets flaky. Imagine a news reader that still shows yesterday’s headlines on the subway, a travel guide that works in a remote cabin, or a productivity app that lets you jot down ideas while your phone’s in airplane mode.

The best part? You get all of this without asking users to install anything from an app store. They just visit your site, and thanks to the service worker, it behaves like a native app—fast, reliable, and offline‑ready.

Give it a try: take a simple project you already have, add a manifest, register a service worker, and precache your core assets. Then toggle your network off and watch the magic happen.

Your turn: What’s the first offline‑first feature you’ll implement? Share your progress in the comments—I’m cheering you on! 🚀

Top comments (0)