completefrontend
Open lab My progress
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
    Follow the lifecycleSay what install, waiting, and activate mean, and what skipWaiting() and clients.claim() do.
  • 02
    Pick a caching strategyChoose cache-first, network-first, stale-while-revalidate, or network-only for each kind of file or request.
  • 03
    Plan for offlineShow an offline page when a page load fails, and say what push and sync need.

A helper between your page and the network

When a page needs a file, it sends a request to the network. The network sends back a response: the file itself.

A service worker is a JavaScript file that the browser runs on its own, apart from your page. It sits between your pages and the network. Every request from those pages reaches the service worker first.

The service worker can then:

  • send the request on to the network, as usual;
  • answer with a copy saved earlier in the cache (a store of saved responses on the user's device);
  • answer with a saved "You are offline" page when there is no network;
  • show a notification when your server sends a message (a push);
  • try a failed job again later (a sync).

A service worker only looks after part of a site, called its scope. For example, the scope /app/ means every page whose address starts with /app/.

The page asks for /styles.css. The request goes to the service worker first. From there an arrow goes up to the network and another goes down to the cache box, which holds styles.css.page/styles.cssservice workernetworkfresh copystyles.csscache
The page's request stops at the service worker first. The worker decides where the answer comes from: the network or the cache.
This site has no lesson service worker

The experiments below only pretend. Some of them read what your browser supports, and nothing more. We never call navigator.serviceWorker.register() here. A real service worker stays in your browser after you leave, and it could take charge of this site on later visits.

Real-life analogyA service worker is like a front desk

Picture an office building where every delivery stops at the front desk first. The person at the desk can send the delivery on, hand over something already stored at the desk, or leave a note when deliveries can't get through.

In real life: The building
In JavaScript: The service worker’s scope
In real life: Offices in the building
In JavaScript: The pages and tabs it looks after
In real life: Deliveries arriving
In JavaScript: Requests the pages send
In real life: The desk handing over something itself
In JavaScript: An answer from the cache, or one the worker makes

This site does not register a service worker. These rows only read what your browser supports, once the page has loaded.

Checking your browserchecking

A service worker uses ideas from earlier lessons: fetch, promises, IndexedDB, and the Cache API. With them, an app can keep working when the network is slow or gone.

Registration & lifecycle

INTERACTIVE

Your page code tells the browser about the worker file. This is called registering it. After that, the worker goes through these stages, in order:

  1. installing: the worker gets ready, for example by saving files in the cache;
  2. installed, also called waiting: it is ready, but not in charge yet;
  3. activating: it clears out old things, such as old caches;
  4. activated: it is the active worker, ready to answer requests.

The worker in charge of a page is called that page's controller. When you publish a new version, it usually waits. It becomes active only after every tab that uses the old version is closed.

Real-life analogyA new version is like a new person at the desk

A well-run building doesn't swap the person at the desk halfway through a delivery. So the new person waits until the old shift ends, unless the building calls for a change right now.

In real life: The new person learns the job
In JavaScript: The install event
In real life: The old person still serves open offices
In JavaScript: The new worker is waiting
In real life: The new person takes over the desk
In JavaScript: The activate event
In real life: Taking over right now
In JavaScript: skipWaiting()
In real life: Also serving offices that had nobody
In JavaScript: clients.claim()
A box called waiting holds v2. A box called active holds v1. An arrow labelled controls goes from the active box to an open tab. v1 is in charge; v2 waits.v2waitingwaitsv1activein chargecontrolsopen tabuses v1
v2 is installed, but an open tab still uses v1. So v2 waits until that tab closes, or until it calls skipWaiting().
Lifecycle playground: v1, v2, waiting, and taking charge
The lifecycle, step by stepTimeline
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
Pretend registrationactive=none waiting=none controller=none tabs=1
Installingnone
Waitingnone
Activenone
Controllernone
  1. No service worker is registered in this pretend scope.
Step 1 of 7empty

This only pretends; no real worker is registered. Try the usual order first: Register v1, Finish v1 install, Publish v2, Finish v2 install, then Close all tabs.

skipWaiting() makes a waiting worker active now, and pages that used the old one switch to it. clients.claim() takes charge of open pages that had no worker.

Service workers only work on secure https pages. The one exception is localhost, your own computer while you build.

By default, the scope is the folder the worker file is in. So /app/sw.js looks after /app/. The browser checks for a new version of the file each time you open a page in the scope. It also checks again if about 24 hours have passed since the last check.

Page code that registers a workerJavaScript
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();      }    });  });}

Line by line:

  • Line 1 checks that the browser has service workers and that the page is secure.
  • Lines 2–3 register /sw.js and ask for the scope /app/.
  • Line 6 listens for updatefound. This event fires when the browser finds a new version. Line 7 gets the new worker while it installs.
  • Line 9 checks two things: the new worker is installed, and the page already has a controller. Then line 10 tells the user an update is ready.

A worker at /app/sw.js starts with the scope /app/. It does not look after the whole site. To ask for a wider scope, the server must allow it with a Service-Worker-Allowed header.

How the default scope is worked outJavaScript
function defaultScope(scriptUrl) {  const path = new URL(scriptUrl).pathname;  return path.slice(0, path.lastIndexOf("/") + 1) || "/";} console.log(defaultScope("https://example.com/app/sw.js"));

Line 2 takes the path from the address: /app/sw.js. Line 3 cuts it after the last /, which leaves the folder /app/.

When a new worker is installed and the page already has a controller, a new version is ready. Show the user an "Update ready" message. Don't change the page under the user without telling them.

This is also why you often need to refresh twice. The first refresh finds and installs the new worker. A later refresh, after the new worker is active, uses it.

When to show an update messagePop out in the code editor (opens in a new tab)JavaScript
function updateMessage(hasController, state) {  if (state !== "installed" || !hasController) return "keep current worker";  return "show update ready";} console.log(updateMessage(true, "installed"));
Choose how the new version takes over

skipWaiting() makes the waiting worker active right away. Open pages that used the old worker switch to the new one. clients.claim() makes the active worker take charge of open pages that had no worker yet.

Both are useful for urgent fixes. But a page loaded with old code may then get new files, and the two may not fit together. An "Update ready" button that the user clicks is often easier to get right.

Answering requests

STEP THROUGH

Once a worker is active and in charge of a page, it gets a fetch event for each request the page makes inside the scope. The event holds the request in event.request.

To answer, the worker calls event.respondWith(). There is one strict rule: call it right away, inside the event handler, not later. The promise you pass to it can still wait for the cache or the network.

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

Choose a network setting, then step through the route a page load takes.

Running in
  1. script
Next: line 1
  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));});
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Network condition

The code stays the same. The setting changes what the pretend networkFirst() promise ends 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.
A worker that picks a route for each requestPop 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));});

Line by line:

  • Line 1 listens for fetch events. Line 2 saves the request in request.
  • Line 4 checks for a page load ("navigate"). Line 5 answers with networkFirst(), with /offline.html as the backup page.
  • Line 9 checks for CSS ("style") or JavaScript ("script"). Line 10 answers with cacheFirst().
  • Line 14 answers every other request with staleWhileRevalidate().
A tag that says fetch event has three arrows. A navigate request goes to networkFirst. A style or script request goes to cacheFirst. Anything else goes to staleWhileRevalidate.fetcheventnavigatestyle, scriptanything elsenetworkFirst()cacheFirst()staleWhileRevalidate()
Each request takes exactly one route. Pages go network first, CSS and JavaScript go cache first, and everything else uses stale-while-revalidate.

The rule is about when you call respondWith(), not when the answer is ready. Calling it right away with a promise is fine. Calling it from a timer is too late. By then, the browser has already decided that this worker is not answering.

Calling respondWith now or laterPop out in the code editor (opens in a new tab)JavaScript
function responseDecision(isDelayed) {  return isDelayed    ? "late respondWith call"    : "promise accepted immediately";} console.log(responseDecision(false));

Notice the return after each respondWith(). Each request gets only one answer.

Notice what is missing, too. A service worker has no document and no page elements (no DOM). It can't change what the page shows directly.

Caching strategies

INTERACTIVE

A caching strategy is a rule for where an answer comes from: the cache, the network, or both. There are four common ones:

  • cache-first: use the saved copy. Go to the network only if there is no copy.
  • network-first: ask the network. Use the saved copy only if the network fails.
  • stale-while-revalidate: answer with the saved copy now, and fetch a new copy for next time. ("Stale" means a bit old; "revalidate" means check for a newer one.)
  • network-only: always ask the network, and never use a saved copy.
Real-life analogyCaching strategies are like kitchen rules

The pantry is the cupboard at home; the shop is down the road. The hard part is not storing food. It is deciding when food from the pantry is still fine, and when you need it fresh from the shop.

In real life: Take it from the pantry first
In JavaScript: cache-first
In real life: Call the shop first; use the pantry if the shop is closed
In JavaScript: network-first
In real life: Serve pantry food now, and buy more for next time
In JavaScript: stale-while-revalidate
Strategy lab: cache, network, and offline page
One function picks the strategyPop 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. Pick the settings, then click Request resource.
Try it yourself

Pick a strategy, a network setting, and a kind of request (asset = a file such as CSS, navigation = a page load, api = data). This only pretends: it never touches your real browser cache.

For a page load, the answer can be /offline.html when the strategy can't get the page any other way.
Which strategy fits?
  • App shell files with a version in their names: CSS, logo, main JavaScript
  • A signed-in dashboard page that should be up to date, with /offline.html when there is no network
  • Profile pictures that can be a little old while a new copy downloads
  • A visit-counting message (analytics) that must not be answered from the cache
  • A documentation site’s /assets/app.ab12.css (ab12 is the version)
  • News feed data that should show the newest items
  • Small product pictures in a shop
  • A payment, or booking an item in stock
Try it yourself
0 of 8 correct

Sort each file or request into the strategy that is safest for it.

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

Offline pages

A good offline app is planned. It does three things, one in each event:

  • install: save a few key files in the cache ahead of time (this is called precaching). These are the app shell (the basic HTML, CSS, and JavaScript the app needs to open) and a simple /offline.html page.
  • activate: delete old caches, so old files don't pile up.
  • fetch: send page loads to the network first. Show the offline page only when the network fails.

Use the offline page only for page loads, where the user needs something to read. A missing image can stay missing. A failed data request should stay an error, so the page can show a "Try again" button.

Be careful with answers from other sites. Your code often can't read them, so it can't check if they worked. Cache them only if you know where they come from and how long they stay correct.

Save files when installing, clean up when activatingPop 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(),    ),  );});

Line by line:

  • Line 5: event.waitUntil() tells the browser "don't finish this stage until this promise is done."
  • Lines 6–7 open the cache app-shell-v3 and save three files in it.
  • Line 14 deletes the old caches app-shell-v1 and app-shell-v2.
  • Line 15 then calls clients.claim(), so open pages with no worker get this one.

Cache names usually include a version, such as v3. When the worker activates, it deletes every old name and keeps the current one.

Finding the old caches to deletePop out in the code editor (opens in a new tab)JavaScript
const currentCache = "app-shell-v3";const cacheNames = ["app-shell-v1", "app-shell-v2", "app-shell-v3"];const oldCaches = cacheNames.filter((name) => name !== currentCache); console.log(oldCaches.join(", "));
Three cache boxes: app-shell-v1 and app-shell-v2 are deleted. app-shell-v3, the current cache, is kept.app-shell-v1deletedapp-shell-v2deletedapp-shell-v3kept
The current cache is app-shell-v3. The two older ones are deleted, so old files don't pile up.
Keep important data in storage

The browser can stop a service worker when it has nothing to do. Then its variables are gone. So save responses with the Cache API, and save lists of jobs in IndexedDB. Don't keep important work in a variable.

Push & background sync

Push and sync don't let your code run in the background whenever it likes. Each one needs extra setup.

A push needs all of these:

  • a server that sends the push message;
  • VAPID keys: a pair of keys that proves the push comes from your server;
  • a subscription: the browser's address that your server sends pushes to;
  • the user's permission to show notifications;
  • userVisibleOnly: true: a promise that every push shows the user a notification.

Background sync lets the browser run a named job again later, for example when the network comes back. It works mainly in Chrome-based browsers, such as Chrome and Edge. So check that the browser has it, and have a backup plan.

Showing a push and running a sync jobPop 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());  }});

Lines 1–7 show a notification when a push arrives. Lines 9–13 run the send-outbox job when the browser fires a sync event.

Sending data is different from reading it. If a message can't be sent yet:

  • save the message before you try again;
  • make sure the server handles a repeat safely, so sending it twice doesn't post it twice;
  • remove it from the saved list only after the server says it got it.

The next code only shows the choice. It doesn't open a real IndexedDB list.

Send now, or save for later?Pop out in the code editor (opens in a new tab)JavaScript
function saveForLater(online, messages) {  return online ? "send now" : "save " + messages.length + " message(s) in IndexedDB";} console.log(saveForLater(false, ["tea"]));

A common plan is an outbox: a saved list of messages waiting to be sent.

  1. Save the message in IndexedDB.
  2. Try to send it.
  3. If the network fails, register a sync job named send-outbox.
  4. When the sync event fires, read the saved messages and try again.

Where you’ll use this

Use a service worker when your app is still useful offline. Good examples are documentation, dashboards with saved data, apps people install on their phone, tools for work away from Wi-Fi, and forms that can send later.

Start small:

  1. cache the app shell;
  2. add an offline page;
  3. then pick a strategy for each kind of file or request.

While you build, open the Application panel in your browser's developer tools. The Service Workers part shows the worker file, its scope, its stage, and an update button. The Cache Storage part shows each cache name and the addresses saved in it. Test a normal online load first. Then tick the "Offline" box and test again.

Decide carefully what to cache. Never put one user's private data where another user could get it. Don't cache a payment result, a permission check, or a stock booking, because the old answer may be wrong now.

A useful update test
  1. Change the cache version and reload once. Check which worker is waiting.
  2. With an old tab still open, test the "Update ready" message.
  3. Close the old tab and reload. Check that the new cache has every file it needs.

Good defaults:

  • Use cache-first for files with a version in their name.
  • Use network-first for pages and for data that should be up to date.
  • Use stale-while-revalidate for images and content that can be a little old.
  • Use network-only for payments, visit counting, and permission checks, or save them in a list to send later.
  • Use the Application panel to see workers, the pages they control, cache names, and an offline test.
  • Before you call an app "ready for offline", test private data, redirects, answers from other sites, and a full cache.

Common misconceptions

The table compares a service worker with two other kinds of script. Read each row across to compare them.

Service workers, web workers, and page scripts side by side
QuestionService workerWeb workerPage script
What is its main job?It catches requests, saves responses, and handles push and sync events.It does slow, heavy work away from the page's main thread.It reads and changes the page.
Can it change the page (the DOM)?No.No.Yes. It runs the page.
How long does it run?It starts for an event and can be stopped when it has nothing to do.It runs while the page that made it keeps it.It runs while the page is open.
Where should it keep data that must last?In the Cache API and IndexedDB.In IndexedDB, or send it to the page.In the page, in memory, or in storage APIs.
  • "A registered worker controls every page." It only controls pages in its scope. Pages that were already open may need clients.claim().
  • "Installed means active." A new worker can be installed and still waiting.
  • "I can call respondWith later." You must call it right away, inside the fetch handler.
  • "Variables keep my data safe." The browser can stop the worker, and its variables are lost. Use the Cache API or IndexedDB.
  • "A worker can change the page." A service worker has no DOM. It can send messages to pages, or answer requests differently.

Practice exercises

4 EXERCISES
Exercise 1 · Warm-upPredict the lifecycle log

Work out what the code prints, then type it in the answer box.

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 CSS file with a version in its name.

    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 bug in the fetch handler

      Explain why this handler doesn't work reliably. Then compare your idea 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

        Plan the strategies for a notes app. It should open offline, show recent notes when it can, and send edits later.

          Quiz

          8 QUESTIONS
          Service workers quiz · 8 questionsScore: first tries count
          1. Question 1 of 8Which sentence best describes a service worker?

            Choose an answer to see the explanation.

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

            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 sentence about scope is true?

            Choose an answer to see the explanation.

          4. Question 4 of 8When must you call respondWith()?

            Choose an answer to see the explanation.

          5. Question 5 of 8Which strategy does this code pick for a versioned CSS file?

            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 shouldn't a service worker keep important data in variables?

            Choose an answer to see the explanation.

          7. Question 7 of 8What does a push notification need?

            Choose an answer to see the explanation.

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

            Choose an answer to see the explanation.

          Key takeaways

          • A service worker sits between the pages in its scope and the network. It wakes up for events and can be stopped when it has nothing to do.
          • Install gets ready. Waiting keeps old tabs safe. Activate cleans up. clients.claim() takes charge of open pages that had no worker.
          • Pick a strategy by how fresh the answer must be, how safe an old answer is, and how useful it is offline.
          • Call respondWith() right away. The promise you give it can do the slow work.
          • Push needs a server. Background sync needs a support check and data saved in IndexedDB.

          Final definition.
          A service worker is a browser script that looks after the pages in its scope. It can answer their requests from the cache, the network, or an offline page, so the app stays useful offline.

          Up next: History & navigation.

          CompleteFrontend Clear concepts. Working examples.