Tips & Tricks (Updated: 6/2/2026)

Claude Code Service Worker Guide for Offline-Ready Apps

Build safe service workers with Claude Code: caching, updates, offline UX, pitfalls, and runnable examples.

Claude Code Service Worker Guide for Offline-Ready Apps

A service worker is one of the most useful browser features for PWA and offline work, but it is also easy to misuse. If you ask Claude Code to “add caching” without boundaries, the app may keep old HTML, cache private account data, or fail to show a new release after deployment.

This guide treats a service worker as a small proxy between the browser and your server. It explains what to decide before using Claude Code, then gives a copy-pasteable vanilla implementation with cache invalidation, update prompts, offline UX, realistic use cases, and concrete failure cases.

Keep the official references open while implementing: MDN Service Worker API, web.dev service worker guidance, web.dev caching guidance, and Chrome Workbox docs. For related ClaudeCodeLab context, read the PWA implementation guide, caching strategies guide, and IndexedDB guide.

What a Service Worker Does

A service worker is JavaScript that runs outside the web page. It cannot touch the DOM, so it cannot directly change buttons, forms, or React state. What it can do is intercept eligible network requests and decide whether to respond from the network, from Cache API, or from a planned offline fallback.

The normal page script ends when the tab closes. A service worker is event-driven: the browser wakes it for install, activate, fetch, push, and sometimes background sync events. For most production apps, the important starting points are fetch handling, Cache API, and the update lifecycle.

sequenceDiagram
  participant User
  participant Page
  participant SW as Service Worker
  participant Cache as Cache API
  participant Net as Server
  User->>Page: Opens the site
  Page->>SW: Registers /sw.js
  Page->>SW: Makes a fetch request
  SW->>Cache: Checks cached response
  alt Cached
    Cache-->>SW: Stored response
  else Not cached
    SW->>Net: Fetches latest response
    Net-->>SW: Fresh response
  end
  SW-->>Page: Response to render

That makes a service worker a traffic controller, not a magic performance switch. The quality comes from choosing what to store, when to delete it, and what users see when the network fails.

Practical Use Cases

Service workers are most valuable when the app has repeat visits, expensive static assets, or users with unreliable networks.

Use caseWhy it helpsMain caution
Documentation or content siteRevisited articles, CSS, images, and fonts load fasterLong-lived HTML can hide fresh edits
SaaS dashboardNavigation shell works on weak connectionsNever cache private billing or account responses
Field input appUsers can keep working with drafts while offlineStore POST work in IndexedDB, not Cache API
Commerce or media catalogThumbnails and static assets avoid repeated downloadsPrices, stock, and protected images need freshness rules

Masa tested the pattern on a small learning site. Caching images and fonts gave a clear repeat-visit improvement. Caching article HTML with a careless Cache First strategy caused a visible bug: typo fixes did not reach returning readers. The lesson is simple: do not ask Claude Code to cache everything. Give it ownership rules.

Brief Claude Code Correctly

Service worker prompts should include prohibited behavior and validation steps. A vague request produces code that may work in a demo while failing in production.

Add a service worker to the existing Vite app.

Requirements:
- Put /sw.js under public and use scope /
- Cache only GET static assets
- Use Network First for HTML navigations
- Return /offline.html when navigation fails offline
- Do not cache APIs, POST requests, auth pages, or other origins
- Include a date or version in the cache name
- Delete old caches during activate
- Show a reload prompt when a new worker is waiting

Verification:
- Check registration in Chrome DevTools > Application > Service Workers
- Switch Network to Offline and confirm /offline.html appears
- Change CACHE_VERSION and confirm old caches are deleted

The explicit exclusions matter. API responses, authenticated HTML, and POST requests are where service worker bugs become data bugs.

Copy-Paste Minimal Implementation

This vanilla example works without a framework. Put these four files in an empty directory such as sw-demo, then start a local server. Service workers require HTTPS or localhost, so opening the HTML file directly is not enough.

python -m http.server 5173

Open http://localhost:5173 in the browser.

<!-- index.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Service Worker Demo</title>
    <style>
      body {
        font-family: system-ui, sans-serif;
        margin: 2rem;
        line-height: 1.7;
      }
      button {
        padding: 0.7rem 1rem;
      }
    </style>
  </head>
  <body>
    <h1>Service Worker Demo</h1>
    <p id="status">Waiting for registration.</p>
    <button type="button" onclick="location.reload()">Reload</button>
    <script src="/register-sw.js"></script>
  </body>
</html>
// register-sw.js
const status = document.querySelector("#status");
let reloadRequested = false;
let updatePromptShown = false;

function setStatus(message) {
  if (status) status.textContent = message;
}

function askToReload(worker) {
  if (updatePromptShown) return;
  updatePromptShown = true;

  const ok = window.confirm(
    "A new version is available. Reload now?",
  );

  if (ok) {
    reloadRequested = true;
    worker.postMessage({ type: "SKIP_WAITING" });
  }
}

async function registerServiceWorker() {
  if (!("serviceWorker" in navigator)) {
    setStatus("This browser does not support service workers.");
    return;
  }

  try {
    const registration = await navigator.serviceWorker.register("/sw.js", {
      scope: "/",
    });

    setStatus(`Service worker registered: ${registration.scope}`);

    if (registration.waiting && navigator.serviceWorker.controller) {
      askToReload(registration.waiting);
    }

    registration.addEventListener("updatefound", () => {
      const worker = registration.installing;
      if (!worker) return;

      worker.addEventListener("statechange", () => {
        const hasOldController = Boolean(navigator.serviceWorker.controller);
        if (worker.state === "installed" && hasOldController) {
          askToReload(worker);
        }
      });
    });
  } catch (error) {
    console.error(error);
    setStatus("Service worker registration failed.");
  }
}

navigator.serviceWorker?.addEventListener("controllerchange", () => {
  if (!reloadRequested) return;
  window.location.reload();
});

registerServiceWorker();
// sw.js
const CACHE_VERSION = "2026-06-02-v1";
const CACHE_PREFIX = "claude-sw-demo";
const CACHE_NAME = `${CACHE_PREFIX}-${CACHE_VERSION}`;

const APP_SHELL = [
  "/",
  "/index.html",
  "/offline.html",
  "/register-sw.js",
];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)),
  );
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((names) =>
      Promise.all(
        names
          .filter((name) => name.startsWith(CACHE_PREFIX))
          .filter((name) => name !== CACHE_NAME)
          .map((name) => caches.delete(name)),
      ),
    ),
  );
  self.clients.claim();
});

self.addEventListener("message", (event) => {
  if (event.data?.type === "SKIP_WAITING") {
    self.skipWaiting();
  }
});

self.addEventListener("fetch", (event) => {
  const { request } = event;
  if (request.method !== "GET") return;

  const url = new URL(request.url);
  if (url.origin !== self.location.origin) return;

  if (request.mode === "navigate") {
    event.respondWith(networkFirstNavigation(request));
    return;
  }

  if (["style", "script", "font", "image"].includes(request.destination)) {
    event.respondWith(staleWhileRevalidate(request));
  }
});

async function networkFirstNavigation(request) {
  const cache = await caches.open(CACHE_NAME);

  try {
    const response = await fetch(request);
    if (response.ok) cache.put(request, response.clone());
    return response;
  } catch {
    return (
      (await cache.match(request)) ||
      (await cache.match("/offline.html")) ||
      new Response("Offline", { status: 503 })
    );
  }
}

async function staleWhileRevalidate(request) {
  const cache = await caches.open(CACHE_NAME);
  const cached = await cache.match(request);

  const fetched = fetch(request)
    .then((response) => {
      if (response.ok) cache.put(request, response.clone());
      return response;
    })
    .catch(() => cached || new Response("Offline", { status: 503 }));

  return cached || fetched;
}
<!-- offline.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>You are offline</title>
  </head>
  <body>
    <main>
      <h1>You are offline</h1>
      <p>Reconnect, then reload this page.</p>
      <button type="button" onclick="location.reload()">Try again</button>
    </main>
  </body>
</html>

This example uses Network First for page navigation and Stale While Revalidate for CSS, JavaScript, fonts, and images. Network First asks the server first and falls back only when the request fails. Stale While Revalidate returns a cached response immediately while refreshing it in the background. Do not use that strategy blindly for news, prices, inventory, or authenticated screens.

Updates and Invalidation

The update lifecycle is where many service worker implementations fail. When sw.js changes, the browser installs a new worker. If an older page is still open, the new worker often waits in the waiting state. The registration code above detects that state, asks the user, and sends SKIP_WAITING only when the user accepts.

The worker then calls self.skipWaiting(), activates, deletes old caches, and claims clients. Without this flow, users can keep an old app-cache-v1 long after you deployed a fix.

Use a cache name that includes a date, release number, or commit ID. If your build emits hashed files, keep the service worker precache list in sync with the build output. When manual cache lists become hard to maintain, Workbox is worth considering. Workbox automates the plumbing, but it still cannot decide which business data is safe to cache.

Offline UX Is Product Work

Offline support is not done when a response comes from Cache API. Users need to know whether their work is saved, waiting to sync, or failed. For forms, do not store POST requests in Cache API. Save drafts or pending jobs in IndexedDB from the page, then retry when the browser is online again. Background Sync can help, but browser support varies, so important flows should also include an online listener and a visible retry button.

When briefing Claude Code, include offline copy, retry controls, draft states, and sync-failure messages. A field app usually needs at least three states: sent, saved on this device, and sync failed.

Common Failure Cases

Scope mismatch is the first common problem. A worker at /app/sw.js controls /app/ by default, not the whole site. If you want full-site control, put the worker at /sw.js and register it with scope /.

cache.addAll() failures are next. If one URL in the list returns 404, the whole install fails. After Claude Code adds files, check DevTools Application panels for registration state and Cache Storage contents.

The third failure is private data. Do not cache /api/me, billing pages, admin HTML, or user-specific JSON unless you have a very deliberate reason and deletion path. Browser cache is still storage on a user’s device, and shared devices make stale personal data visible.

The fourth failure is skipping update UX. Old workers can hold old JavaScript and CSS. Version your cache names, delete old caches during activate, and let users reload when a new worker is waiting.

Finally, service workers are not permanent storage. Browsers may evict caches under pressure. Opaque cross-origin responses can be hard to size and debug. Service workers cannot access the DOM. They run only on HTTPS or localhost. Promise less than “complete offline support” unless you have tested the real workflow.

Summary and CTA

A service worker can improve repeat visits, offline behavior, and PWA quality. The safe path is to decide cache ownership, update lifecycle, private-data rules, and offline screens before asking Claude Code to edit files.

ClaudeCodeLab can help with PWA conversion, cache design, offline forms, Workbox migration, and Claude Code implementation reviews. If your site needs to become faster without serving stale or private data, start from Claude Code training and consulting.

When this minimal setup is tested in local Chrome, the Application panel shows claude-sw-demo-2026-06-02-v1 after the first load. Switching Network to Offline and reloading displays offline.html. Changing CACHE_VERSION causes old caches to be deleted during activate, which makes it a practical starting point for release verification.

#Claude Code #Service Worker #PWA #offline #caching
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.