cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Service workers & offline apps

Learn how service workers intercept requests, manage their lifecycle, choose caching strategies, show offline fallbacks, and support push or background sync without registering one on this site.

By the end, you can
  • 01
    Trace the lifecycleExplain install, waiting, activate, skipWaiting, and clients.claim.
  • 02
    Pick a cache strategyMatch resources to cache-first, network-first, stale-while-revalidate, or network-only.
  • 03
    Design offline behaviorRoute navigations to a fallback page and know what push or sync require.

A tiny server in the browser

A service worker is a JavaScript file the browser can run between pages in its scope and the network. It can intercept requests, answer from cache, fall back to a saved offline page, receive push events, and retry queued work later.

Important safety note

This site has no lesson service worker. The experiments below are faithful in-page simulations plus read-only feature detection. We do not call navigator.serviceWorker.register() here because a real registration persists and could control future visits.

Real-life analogyA service worker is a front-desk concierge

Imagine every package for a building stops at the front desk. The concierge can forward it to the store, hand over something already in the pantry, or give a helpful note when deliveries are impossible.

In real life: The building
In JavaScript: The service worker’s scope
In real life: Offices in the building
In JavaScript: Controlled pages or tabs
In real life: Deliveries arriving
In JavaScript: Fetch requests
In real life: The concierge answering from the desk
In JavaScript: A cached or generated Response

Where the analogy stops: A concierge is always standing there. A real service worker is event-driven and the browser may stop it when idle, so durable state belongs in Cache API or IndexedDB.

This site does not register a service worker. These are read-only browser facts detected after mount.

Waiting for browserchecking

You have seen fetch, promises, IndexedDB, and the Cache API. A service worker combines those ideas into a request router that can keep an app useful when the network is unreliable.

Registration & lifecycle

INTERACTIVE

Real apps register a worker from page code. The worker then moves through installing, installed/waiting, activating, and activated. A new version usually waits until pages using the old version close.

Real-life analogyThe lifecycle is a shift change

A responsible building does not swap the front desk mid-delivery unless it chooses an emergency handoff. That is why version two can be installed but waiting while version one still controls open tabs.

In real life: New concierge trains
In JavaScript: install event
In real life: Current shift is still serving offices
In JavaScript: waiting worker
In real life: New concierge takes the desk
In JavaScript: activate event
In real life: Taking over immediately
In JavaScript: skipWaiting()
In real life: Greeting offices already open
In JavaScript: clients.claim()

Where the analogy stops: A worker can activate without controlling an already-open page unless clients.claim() runs after activation. Activation and control are related, not identical.

Lifecycle simulator: v1, v2, waiting, and control
Lifecycle actionsTimeline
register("/sw.js")install event: precache filesactivate event: cleanup old caches// Later: /sw.js changes on the servernew worker installsnew worker waits while old tabs are openskipWaiting(); // optional immediate handoffclients.claim(); // optional control for open pages
Simulated registrationactive=none waiting=none controller=none tabs=1
Installingnone
Waitingnone
Activenone
Controllernone
  1. No service worker is registered for this simulated scope.
Step 1 of 7empty

This is a tested pure state machine, not a real registration. Try the normal path first: register v1, finish it, deploy v2, finish it, then close all tabs.

skipWaiting() activates a waiting worker. clients.claim() makes open pages controlled after activation.

Service workers require HTTPS, except on localhost for development. The default scope is the script’s directory: /app/sw.js controls /app/ by default. Browsers check for updates on navigation to a controlled page and periodically, at least about every 24 hours.

Page code in a real appJavaScript
if ("serviceWorker" in navigator && isSecureContext) {  const registration = await navigator.serviceWorker.register("/sw.js", {    scope: "/app/",  });   registration.addEventListener("updatefound", () => {    const next = registration.installing;    next?.addEventListener("statechange", () => {      if (next.state === "installed" && navigator.serviceWorker.controller) {        showUpdateReadyMessage();      }    });  });}

Fetch events and scope

STEP THROUGH

Once active and controlling a page, the worker receives fetch events for requests in scope. The key rule is precise: event.respondWith() must be called synchronously inside the handler. The promise you pass can do asynchronous cache and network work afterward.

Step through a fetch event
Step 0 of 6Ready
Your turn: follow the blue line

Choose a network condition, then step through the exact fetch route a navigation request takes.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  const request = event.request;   if (request.mode === "navigate") {    event.respondWith(networkFirst(request, "/offline.html"));    return;  }   if (request.destination === "style" || request.destination === "script") {    event.respondWith(cacheFirst(request));    return;  }   event.respondWith(staleWhileRevalidate(request));});
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Network condition

The source stays the same. The setting changes which branch the simulated networkFirst() promise resolves with.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Realistic fetch routing in sw.jsPop out in the code editor (opens in a new tab)JavaScript
self.addEventListener("fetch", (event) => {  const request = event.request;   if (request.mode === "navigate") {    event.respondWith(networkFirst(request, "/offline.html"));    return;  }   if (request.destination === "style" || request.destination === "script") {    event.respondWith(cacheFirst(request));    return;  }   event.respondWith(staleWhileRevalidate(request));});

Notice the early return after each respondWith(). A request gets one response decision. Also notice what is missing: there is no document, no DOM, and no React state inside the worker.

Caching strategies

INTERACTIVE
Real-life analogyCaching strategies are pantry rules

The hardest part of offline apps is not writing cache.put(). It is choosing when old data is acceptable and when a live answer matters.

In real life: Grab it from the pantry first
In JavaScript: cache-first
In real life: Call the store first, pantry if the store is closed
In JavaScript: network-first
In real life: Serve pantry food now, restock in the background
In JavaScript: stale-while-revalidate

Where the analogy stops: Food does not have privacy headers, authentication, or correctness rules. Real apps must avoid caching sensitive or always-fresh responses with the wrong strategy.

Strategy lab: cache, network, and fallback
Strategy routerPop out in the code editor (opens in a new tab)JavaScript
async function handleRequest(request, strategy) {  if (strategy === "cache-first") return cacheFirst(request);  if (strategy === "network-first") return networkFirst(request, "/offline.html");  if (strategy === "stale-while-revalidate") return staleWhileRevalidate(request);  return fetch(request);}
Resultcache-first · online · asset

No request yet.

  1. Choose settings, then request a resource.
Try it yourself

No real browser service worker or CacheStorage is used here. The pure model uses real Request and Response objects in tests.

Navigation requests can fall back to /offline.html when the chosen strategy cannot produce a page.
Which strategy fits?
  • App shell files: CSS, logo, main JavaScript bundle
  • A logged-in dashboard page that should be fresh but needs /offline.html when disconnected
  • User avatars that can be a little old while a new copy downloads
  • Analytics beacon that should not be replayed from cache
  • A documentation site’s versioned /assets/app.ab12.css
  • News feed API data that should prefer the newest items
  • Product thumbnail images on a catalog
  • Payment request or stock reservation
Try it yourself
0 of 8 correct

Sort each resource by the safest default strategy.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Offline fallbacks

Offline support should feel intentional. Precache a small app shell and a clear /offline.html page during install. During activate, delete old cache names so old files do not pile up forever. During fetch, send navigation requests through network-first and fall back to the offline page only when necessary.

Install precache and activate cleanupPop out in the code editor (opens in a new tab)JavaScript
const STATIC_CACHE = "app-shell-v3";const OLD_CACHES = ["app-shell-v1", "app-shell-v2"]; self.addEventListener("install", (event) => {  event.waitUntil(    caches.open(STATIC_CACHE).then((cache) =>      cache.addAll(["/", "/styles.css", "/offline.html"]),    ),  );}); self.addEventListener("activate", (event) => {  event.waitUntil(    Promise.all(OLD_CACHES.map((name) => caches.delete(name))).then(() =>      self.clients.claim(),    ),  );});
Keep state somewhere durable

The browser can stop a service worker when it is idle. Cache responses in the Cache API and store structured queues in IndexedDB. Do not rely on a global array to remember important work.

Push & background sync

Push and sync are not magic background JavaScript. Push requires a server, VAPID keys, a subscription, notification permission, and userVisibleOnly. Background sync retries named work later, mainly in Chromium browsers, so feature detection and fallbacks still matter.

Push and one-shot sync modelPop out in the code editor (opens in a new tab)JavaScript
self.addEventListener("push", (event) => {  event.waitUntil(    self.registration.showNotification("New message", {      body: event.data?.text() ?? "Open the app to read it.",    }),  );}); self.addEventListener("sync", (event) => {  if (event.tag === "send-outbox") {    event.waitUntil(sendQueuedMessagesFromIndexedDB());  }});

A common pattern is an outbox: save a draft message in IndexedDB, try sending it, and if the network fails, register a sync event named send-outbox. When sync fires, read IndexedDB and retry.

Where you’ll use this

Use service workers when the app has a useful offline shape: documentation, dashboards with cached data, installable PWAs, field tools, or forms that can queue work. Start small: cache the app shell, add an offline page, then choose strategies resource by resource.

  • Version static assets and serve them cache-first.
  • Use network-first for HTML and freshness-sensitive API data.
  • Use stale-while-revalidate for images and content that can be briefly old.
  • Keep payments, analytics, and permission checks network-only or explicitly queued.

Common misconceptions

Service workers compared with nearby browser scripts
FeatureService workerWeb workerPage script
Main jobIntercept requests, cache responses, receive push/sync eventsRun CPU work away from the main threadRead and change the DOM
Can use the DOM?No DOM accessNo DOM accessYes, it owns the page UI
LifetimeCan start for an event and stop when idleRuns while the owning page keeps itRuns while the page is loaded
Storage for durable stateCache API and IndexedDBIndexedDB or messages to the pageDOM state, memory, storage APIs
  • “A registered worker controls everything.” Scope limits control, and an active worker may not control already-open pages unless claimed.
  • “Install means active.” A new worker can be installed and waiting.
  • “I can call respondWith later.” The call must happen synchronously in the fetch handler.
  • “Globals are safe storage.” The browser can stop the worker; use Cache API or IndexedDB.
  • “Workers can update the DOM.” Service workers have no DOM. Send messages to pages or respond with different resources.

Practice exercises

4 EXERCISES
Exercise 1 · Warm-upPredict the lifecycle log

Run the code mentally before using the answer checker.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const events = [];
events.push("install v1");
events.push("activate v1");
events.push("install v2");
events.push("v2 waits");
console.log(events.join(" -> "));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticePick the strategy

    Choose the strategy for a fingerprinted CSS file.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const resource = "versioned CSS";
    const strategy = resource === "versioned CSS" ? "cache-first" : "network-first";
    console.log(strategy);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind the fetch-handler bug

      Explain why the original handler is unreliable, then compare with the solution.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      self.addEventListener("fetch", (event) => {
        setTimeout(() => {
          event.respondWith(fetch(event.request));
        }, 0);
      });
        Exercise 4 · ChallengePlan a small offline app

        Design a strategy list for a notes app that should open offline, show recent notes if available, and send edits later.

          Quiz

          8 QUESTIONS
          Service workers quiz · 8 questionsScore: first tries count
          1. Question 1 of 8What is the safest description of a service worker?

            Choose an answer to see the explanation.

          2. Question 2 of 8What does this lifecycle log show?

            Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
            const events = [];
            events.push("install v1");
            events.push("activate v1");
            events.push("install v2");
            events.push("v2 waits");
            console.log(events.join(" -> "));

            Choose an answer to see the explanation.

          3. Question 3 of 8Which fact about scope is true?

            Choose an answer to see the explanation.

          4. Question 4 of 8What does respondWith() need?

            Choose an answer to see the explanation.

          5. Question 5 of 8Which strategy fits versioned app-shell files best?

            Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
            const resource = "versioned CSS";
            const strategy = resource === "versioned CSS" ? "cache-first" : "network-first";
            console.log(strategy);

            Choose an answer to see the explanation.

          6. Question 6 of 8Why should a service worker not keep important state in globals?

            Choose an answer to see the explanation.

          7. Question 7 of 8What do push notifications require?

            Choose an answer to see the explanation.

          8. Question 8 of 8Which statement about this site is true?

            Choose an answer to see the explanation.

          Key takeaways

          • A service worker is a scoped request interceptor with an event-driven lifetime.
          • Install prepares, waiting protects old tabs, activate cleans up, and control may need clients.claim().
          • Pick strategies by freshness, safety, and offline usefulness.
          • Call respondWith() synchronously, then let promises do asynchronous work.
          • Push needs server infrastructure; background sync needs feature detection and durable queued data.

          Final definition.
          A service worker is a scoped, event-driven browser worker that can answer network requests from cache, network, or fallbacks so an app stays useful offline.

          Up next: History & navigation.

          CompleteFrontend Clear concepts. Working examples.