completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

Service workers and performance

A service worker can make return visits instant or slower. Measure its wake-up, use navigation preload, pick caching strategies, and skip it with static routes.

By the end, you can
  • 01
    Measure what a worker costsRead fetchStart minus workerStart, and tell a visit that woke the worker from one that found it awake.
  • 02
    Keep the network from waitingTurn on navigation preload and use its answer, and send requests around the worker with static routes.
  • 03
    Pick a strategy for each requestAnswer versioned files cache first, and choose how fresh each page must be, then check the photo moved.

A return visit, with and without a worker

Asha is on the bus again. She opens the Leaf and Kettle tea list, as she does most mornings. Each time, the screen stays empty for most of a second. Then the teas appear, with the mint tea photo on top.

Ravi wants her return visits to feel instant. He reads that a service worker can do that. He also reads that one can make a visit slower. So which is it?

A service worker is a script the browser runs apart from the page. Once it's installed, it sits between the page and the network. Every request the page makes reaches it first. It can answer from its own store, Cache Storage, or pass the request on to the network. Lesson 15.3 met it as a cook with her own pantry.

Ravi builds six versions of the shop's worker. The course recorded Asha's return visits to each, and to the shop with no worker at all. Here is what she saw on three of them:

Asha's screen in real frames on her return visit, with the worker asleep, at three moments. At 110 ms: No worker, nothing on the screen yet; Worker passes requests on, nothing on the screen yet; Worker answers from its cache, the tea list, with the mint tea photo. At 780 ms: No worker, the tea list, with the mint tea photo; Worker passes requests on, nothing on the screen yet; Worker answers from its cache, the tea list, with the mint tea photo. At 810 ms: No worker, the tea list, with the mint tea photo; Worker passes requests on, the tea list, with the mint tea photo; Worker answers from its cache, the tea list, with the mint tea photo.110 ms780 ms810 msNo workernothingyetthe tea list, with the mint tea photothe tea list, with the mint tea photoWorker passesrequests onnothingyetnothingyetthe tea list, with the mint tea photoWorker answersfrom its cachethe tea list, with the mint tea photothe tea list, with the mint tea photothe tea list, with the mint tea photo
Real frames from Asha's phone, from the middle visit of five. An empty window means nothing had been drawn yet. The worker that answers from its cache showed the page first. The one that passes requests on showed it last.

Without a worker, the page came at about 780 ms. With a worker that passes every request on, it came a little later. With a worker that answers from its cache, it came at about 110 ms. Here are the numbers behind those screens:

Asha's return visit, three waysPop out in the code editor (opens in a new tab)JavaScript
// Asha's return visit, worker asleep. Medians of 5 visits, in ms.const noWorker = { firstByte: 677.6, photo: 776 };const passThrough = { firstByte: 681.6, photo: 804 };const fromCache = { firstByte: 8.5, photo: 108 }; console.log(`No worker: the photo at ${noWorker.photo} ms`);console.log(`A worker that passes requests on: ${passThrough.photo} ms`);console.log(`A worker that answers from its cache: ${fromCache.photo} ms`);
  • Line 1 is a comment. Each number is a median of five visits: the middle one, once they're sorted. Times are in milliseconds (ms), thousandths of a second.
  • Line 2 is the shop with no worker. firstByte is when the first byte of the page arrived. photo is when the photo appeared: the Largest Contentful Paint.
  • Line 3 is a worker that passes every request on. Line 4 is a worker that answers from Cache Storage.
  • Lines 6 to 8 print No worker: the photo at 776 ms, A worker that passes requests on: 804 ms and A worker that answers from its cache: 108 ms.

So the answer is both. The worker that answered from its cache showed the photo about 7 times sooner. The worker that only passed requests on gained nothing, and lost a little. Why it matters: a worker isn't fast or slow by itself. What it does with each request decides.

Real-life analogyThe night porter at a small hotel

You arrive late at a small hotel. The night porter is asleep, so you ring and wait. If your key is on the board behind him, you're in your room a minute later. But if he has to phone the owner for every key, waking him only added a wait.

In real life: The night porter
In web performance: The service worker
In real life: Waking him up
In web performance: Starting a stopped worker
In real life: The keys on the board behind him
In web performance: Cache Storage
In real life: Phoning the owner for every key
In web performance: Passing each request on to the network
The main idea

A service worker makes a return visit faster only when it answers from its own cache. For everything else, it should stay out of the way, or let the network start without it. Waking it costs time, and the browser can hide that cost only some of the time.

How the course recorded it

  • Asha's phone, as in lesson 24.2: Chrome 157 on a phone's screen, with the page's CPU 4 times slower. The network has Slow 4G's round trip and speed, on lesson 16.3's link. That link slows every request the same way, the worker's own included.
  • The page: the tea list, which the server takes 200 ms to build. Its stylesheet, script and photo are cached for a year (lesson 15.1).
  • Three visits each time: a first visit, when the worker installs. Then a return visit with the worker stopped, which this lesson calls asleep. Then one with it still running: awake. Before each return visit, the tab and its connection were closed, as when Asha comes back later.
  • Meera adds a tea. After Asha's first visit, lemon verbena joins the list. So a fresh page has 9 teas, and an old copy has 8.

Service workers & offline apps in the JavaScript course builds a worker and explains its life, from install to activate. The system design lesson Service worker caching plans one for a whole app. This lesson measures what one does to speed. It answers five questions:

  • What does waking a worker cost? Worker startup.
  • Can the network start while it wakes? Navigation preload.
  • Which answers should come from the cache? Caching strategies.
  • Can some requests skip the worker? The static routing API.
  • How do you know it helped? Measuring repeat visits.

What waking a worker costs

A worker doesn't run all the time. The spec lets the browser stop it whenever it has no event to handle. The next request has to start it again. Until it's running, it can't take the request.

Every visit has a navigation entry: the list of times the browser writes down as the page loads. It keeps two times for this. Lesson 5.1 met the first one:

  • workerStart is when the browser handed the request to the worker. If the worker was asleep, that's when it started waking it. It's 0 when no worker took the request.
  • fetchStart comes next. In Chrome, with a worker, it's when the worker was ready to take the request.

So the gap between them is the time the request waited for the worker. Here are two of Asha's visits:

How long the request waited for the workerPop out in the code editor (opens in a new tab)JavaScript
// Two return visits with the pass-through worker, from the navigation entry, in ms.const asleep = { workerStart: 2.6, fetchStart: 5.8 };const awake = { workerStart: 1.2, fetchStart: 1.3 }; const wait = (visit) => (visit.fetchStart - visit.workerStart).toFixed(1);console.log(`Worker asleep: the request waited ${wait(asleep)} ms`);console.log(`Worker awake: ${wait(awake)} ms`);
  • Lines 2 and 3 hold the two times from two visits with the pass-through worker: one asleep, one awake.
  • Line 5 subtracts workerStart from fetchStart.
  • Lines 6 and 7 print Worker asleep: the request waited 3.2 ms and Worker awake: 0.1 ms.

Three milliseconds is tiny. That's because the worker ran on a laptop's fast CPU. The recorder slowed the page's CPU 4 times, but DevTools' slowdown doesn't reach a worker. Chrome refuses it there. The course checked with the same loop. It took 485.2 ms on the slowed page, 121.9 ms in the worker, and 117.2 ms on the page without the slowdown.

On real phones it takes longer. web.dev says starting a worker usually takes "around 50ms. On mobile it's more like 250ms." On slow devices, it can take over 500 ms.

Chrome hid the wait, this time

Here's the pass-through worker. It hands every request straight back to the network:

sw.js: the pass-through workerJavaScript
self.addEventListener("fetch", (event) => {
  event.respondWith(fetch(event.request));
});

Line 1 listens for every request. Line 2 answers each one with respondWith(). The answer comes from fetch(): a normal request to the network.

With the worker asleep, you'd expect the page's request to leave late. It didn't. The server got it at 395.4 ms, the same moment as with no worker at all: 394.6 ms. Chrome had sent it while the worker was waking.

Chrome calls this auto preload. When a sleeping worker has to wake for a page, Chrome starts the page's request at the same time. Then the worker's fetch() gets that request's answer. Chrome does this only while the worker is asleep. The spec allows it, but doesn't ask browsers for it.

So did the pass-through worker cost nothing? Not quite. It gained nothing either. The photo came at 804 ms, against 776 ms with no worker. Every file still came from the browser's HTTP cache, only now through the worker. Why it matters: a worker that passes everything on is all cost and no gain. If a worker only does push messages, give it no fetch handler. Then the spec lets browsers skip it for every request, "to avoid unnecessary delays".

Pick a version, and compare a visit with the worker asleep to one with it awake:

Playground: one version, asleep and awake
What the recorder measured
  1. AsleepAwake
  2. Waiting for the worker3.2 ms0.1 ms
  3. First byte of the page681.6 ms679.8 ms
  4. Photo on screen804 ms776 ms
  5. Where the page came fromThe networkThe network
  6. Requests for the page at the server11
  7. Teas on the list99
Try it yourself
Pass-through worker

Asha's return visits with this version: on the left with the worker stopped first, on the right with it still running.

Chrome 157 on a phone's screen, the page's CPU 4 times slower, and Slow 4G's round trip and speed on lesson 16.3's link. Medians of five visits.

Navigation preload: ask while the worker wakes

Auto preload is Chrome's own trick. The standard way to start the request early is navigation preload. It works in Chrome, Firefox and Safari.

Real-life analogyPutting the kettle on first

You make tea for a friend. You could find the cups first, then fill the kettle. Or you switch the kettle on first, and find the cups while it boils. The tea is ready sooner, and the cups were no faster to find.

In real life: Filling and switching on the kettle
In web performance: Sending the request for the page
In real life: Finding the cups and the tea
In web performance: Waking the worker
In real life: Doing both at once
In web performance: Navigation preload
In real life: The kettle, boiled and ready
In web performance: event.preloadResponse

A worker turns it on once, when it becomes the active worker:

Turn navigation preload onJavaScript
self.addEventListener("activate", (event) => {
  event.waitUntil(self.registration.navigationPreload.enable());
});

Line 1 runs when the worker activates. Line 2 calls navigationPreload.enable(). From now on, the browser sends each page's request at once, while the worker wakes. It adds a header, so the server can tell: Service-Worker-Navigation-Preload: true.

The worker must then use that answer. Here is Ravi's network-first function for the page:

Use the preloaded answerJavaScript
async function networkFirst(event) {  try {    const response = (await event.preloadResponse) ?? (await fetch(event.request));    event.waitUntil(keepCopy(event.request, response.clone()));    return response;  } catch {    return caches.match(event.request);  }}
  • Line 3 waits for event.preloadResponse: the answer to the request the browser already sent. If there's none, it fetches the page itself.
  • Line 4 keeps a copy in Cache Storage, in the background, with keepCopy(), a small function of Ravi's. Line 5 gives the page to Asha.
  • Lines 6 and 7 run only if the network fails. Then the stored copy answers instead.

With the worker asleep, the server got the request at 394.6 ms, with the header. The first byte reached Asha at 682.8 ms. The same worker without preload got 680.7 ms: Chrome's auto preload had already done the same job. Chrome picks navigation preload over auto preload when both could run.

Why it matters: in browsers without auto preload, the request would wait for the whole wake-up. On a phone, that's a quarter of a second on every visit with the worker asleep. Use navigation preload for pages that come from the network. Chrome's Workbox docs add one warning. A worker that answers pages from its cache doesn't need it: the preloaded answer goes to waste.

Try a slower wake-up on the recorded visits:

Playground: a slower wake-up
When the first byte would arrive
  1. No worker678.8 ms
  2. Network first, no preload of any kind679.4 ms
  3. Network first, with navigation preload680.8 ms
  4. Stale-while-revalidate, from the cache2.4 ms
Try it yourself
First byte of the page, if waking took 0 ms

This lab records nothing. It takes Asha's recorded visits with the worker awake, and adds the wake-up you pick. Without any preload, the request waits for all of it. With navigation preload, the request leaves at once, so the wake-up only matters if it's longer than the trip.

Arithmetic on the recorded medians, not a new recording.

Preload switched on, but never used

One more worker turned preload on, but kept the old network-first function. That function calls fetch(event.request) and never reads event.preloadResponse. Here's what the server got:

Preload on, and a second requestPop out in the code editor (opens in a new tab)JavaScript
// Preload is on, but the worker fetches the page itself. Medians of 5 visits, in ms.const requests = [  { path: "/teas", at: 395.9, preload: true },  { path: "/teas", at: 758, preload: false },]; console.log(`The server built the tea list ${requests.length} times.`);console.log(`The second request came ${(requests[1].at - requests[0].at).toFixed(1)} ms later.`);
  • Lines 3 and 4 are the two requests for the tea list, with when each reached the server.
  • Line 7 prints The server built the tea list 2 times.
  • Line 8 prints The second request came 362.1 ms later.

The worker gave Asha the answer to its own request, the second one. Her first byte came at 1,043.1 ms. That's 360 ms later than with preload used. web.dev warns about exactly this: use fetch(event.request) instead of the preloaded answer, and every page is requested twice.

Four versions of the shop, on a time line from 0 to 1,000 ms, with the worker asleep: medians of five visits. No worker: the server got the page's request at 394.6 ms, and the first byte reached Asha at 677.6 ms. Network first: the server got the page's request at 394.8 ms, and the first byte reached Asha at 680.7 ms. With preload: the server got the page's request at 394.6 ms, and the first byte reached Asha at 682.8 ms. Preload unused: the server got 2 requests for the page at 395.9 ms and 758 ms, and the first byte reached Asha at 1,043.1 ms.No worker677.6 msNetwork first680.7 msWith preload682.8 msPreload unused1,043.1 ms0250 ms500 ms750 ms1,000 ms
Each bar runs from the start of the visit to the page's first byte. Each dashed line is a request for the page reaching the server. With preload unused, the server got two, and Asha waited for the second.

Caching strategies for speed

So far, every page came from the network, and its first byte took at least 677 ms. Only an answer from Cache Storage skips that trip. A caching strategy is the rule a worker follows for a request: the cache first, the network first, or both.

Real-life analogyThree ways to check the bus times

Asha needs the time of the next bus. The printed timetable in her bag is instant, but it may be old. The live app is always right, but it needs signal. Or she can read the printed one now, and print a new one at home tonight.

In real life: The printed timetable in your bag
In web performance: Cache first
In real life: The live app, with the paper copy as a backup
In web performance: Network first
In real life: Read the paper copy now, and refresh it at home tonight
In web performance: Stale-while-revalidate

The JavaScript lesson listed these strategies. Here they meet Asha's phone. The page's three files have versions in their names, so the worker answers them cache first:

Cache first, for versioned filesJavaScript
async function cacheFirst(request) {
  const cached = await caches.match(request);
  return cached ?? fetch(request);
}

Line 2 looks for a stored copy. Line 3 returns it, or fetches the file if there's none. Workbox's docs suggest this for static files, "especially hash-versioned ones". A new version gets a new name, so a stored copy is never wrong.

The page itself changes. Meera just added a tea. For the page, Ravi tried stale-while-revalidate: answer from the cache at once, and fetch a fresh copy for next time.

Stale-while-revalidate, for the tea listJavaScript
async function staleWhileRevalidate(event) {  const cached = await caches.match(event.request);  const fresh = fetch(event.request).then(async (response) => {    await keepCopy(event.request, response.clone());    return response;  });  event.waitUntil(fresh);  return cached ?? fresh;}
  • Line 2 looks for the stored page.
  • Lines 3 to 6 start a fresh request. When it arrives, they store the copy, and return it.
  • Line 7 keeps the worker running until that's done. waitUntil() asks the browser not to stop it yet.
  • Line 8 answers with the stored page, or with the fresh one if nothing was stored.

With the worker asleep, the first byte came at 8.5 ms, and the photo at 108 ms. With it awake: 2.4 ms and 100 ms. Here is where the wake-up shows. An answer from the cache can't leave until the worker is running. No preload can hide that.

But Asha saw 8 teas, not 9. That's the price of stale-while-revalidate: it shows the copy from an earlier visit. The network-first worker showed all 9 teas, and was no faster than no worker: its photo came at 772 ms.

And there was a surprise. With the worker asleep, the refresh never reached the server. Not once in five visits. Chrome had started the page's request itself, with auto preload. So the worker's fetch() got that request. Chrome's notes say it may cancel that request once the worker has answered. The worker answered from its cache in 8.5 ms, long before the connection was open. So the old list stayed for one more visit.

Read each strategy across: where Asha's answer comes from, how fresh it is, and what it suits.

Four strategies for one request
StrategyWhere Asha's answer comes fromHow fresh it isGood for
Cache firstCache Storage. The network only if the file is missing.It changes only when the worker stores a new copy.Versioned files: styles, scripts, the photo.
Network firstThe network. Cache Storage only if the network fails.Always the newest.Pages and data that must be current, like an order's status.
Stale-while-revalidateCache Storage at once. The network refreshes it in the background.One visit behind.Pages where a slightly old copy is fine, like a list of products.
Network onlyThe network. The worker keeps no copy.Always the newest.Checkout and payments.

Why it matters: pick a strategy for each kind of request, not one for the whole site. web.dev suggests stale-while-revalidate for lists of products, and network first for prices and order status. And check that your refresh really reaches the server.

Static routes: skip the worker

Cache first still wakes the worker, just to look in a cache. The static routing API lets a worker write its rules down at install time. Then the browser follows them by itself, without running the worker's code.

Real-life analogyA note on the porter's door

Before the night porter goes to sleep, he pins a note to his door. "Parcels: leave them in the porch. Late guests: the side door is open." Couriers and guests read the note and carry on. Nobody has to wake him.

In real life: The note, written before he goes to sleep
In web performance: Routes added at install, with addRoutes()
In real life: "Parcels: leave them in the porch"
In web performance: Files from Cache Storage
In real life: "Late guests: use the side door"
In web performance: The page from the network while the worker sleeps
In real life: Nobody rings his bell
In web performance: The worker isn't asked

Here is Ravi's last worker. Its install step adds two routes:

Two static routesJavaScript
self.addEventListener("install", (event) => {  event.addRoutes?.([    { condition: { urlPattern: "/assets/*" }, source: { cacheName: "assets" } },    { condition: { requestMode: "navigate", runningStatus: "not-running" }, source: "network" },  ]);  event.waitUntil(precache());});
  • Line 2 calls event.addRoutes(). The ?. skips it in browsers that don't have it.
  • Line 3 is the first route. Any file under /assets/ comes from the cache named assets. If it isn't there, it comes from the network.
  • Line 4 is the second. A page request goes straight to the network, but only when the worker is "not-running". When it's awake, the worker's own code answers.
  • Line 6 stores the files, as before.

Each route has a condition, which says which requests it matches, and a source, which says where the answer comes from. Conditions can test the address, the method, the mode, the destination and whether the worker is running. Sources can be the network, the cache, the worker's code, or a race between the network and the worker. The first route that matches wins.

Here is what Chrome reported for two of the visit's requests:

What the routes didPop out in the code editor (opens in a new tab)JavaScript
// Two entries from Asha's visit with static routes, worker asleep.const page = { workerStart: 0, workerMatchedSourceType: "network" };const photo = { workerMatchedSourceType: "cache", deliveryType: "cache-storage" }; console.log(`The page: ${page.workerMatchedSourceType}, workerStart ${page.workerStart}`);console.log(`The photo: ${photo.workerMatchedSourceType}, from ${photo.deliveryType}`);
  • Line 2 is the page. workerMatchedSourceType says which route's source matched: the network. workerStart is 0, because the worker never took the request.
  • Line 3 is the photo. It matched the cache route, and deliveryType says it came from Cache Storage.
  • Lines 5 and 6 print The page: network, workerStart 0 and The photo: cache, from cache-storage.
Where Asha's four requests went with static routes and the worker asleep. Route 2 sent the page, /teas, to the network, and the server got it at 393.7 ms. Route 1 sent style.v1.css, shop.v1.js and mint-tea.v1.jpg to the cache named assets in Cache Storage. The worker answered none of them. Chrome woke it in the background.Asha's requests/teasstyle.v1.cssshop.v1.jsmint-tea.v1.jpgthe networkgot the page at 393.7 msCache Storagethe cache named assetsroute 2: not-runningroute 1: /assets/*the workernot asked; Chrome woke it in the background
With the worker asleep, route 2 sent the page to the network, and route 1 sent the three files to Cache Storage. The worker answered none of the four requests.

The server got the page at 393.7 ms, as with no worker. The photo came at 768 ms. Chrome still woke the worker, in the background, so it was ready for the page's next requests. Its source code says this start is "not on the critical path".

Why it matters: routes keep the worker off the path of requests it would only pass on. That helps most where waking is slow. On the course's fast laptop, they saved little. But unlike auto preload, they're in the standard, and Safari has them too.

Measuring the gain on repeat visits

The recorder knew whether each worker was asleep. A visitor's navigation entry has no field that says so. So measure the parts that show it.

Real-life analogyTiming a new route to work

Ravi tries a new route to the shop. On cold mornings the car takes a while to warm up. If he compares his new route on warm afternoons with the old one on cold mornings, the new one looks great, and it's not the route's doing.

In real life: Cold mornings, when the car needs warming up
In web performance: Visits with the worker asleep
In real life: Trips after a short stop
In web performance: Visits with the worker awake
In real life: Your phone's trip log
In web performance: The navigation entry's times
In real life: Comparing Mondays with Mondays
In web performance: Comparing asleep with asleep

A few lines on the page can send what each visit saw:

Send each visit's worker timesJavaScript
const [visit] = performance.getEntriesByType("navigation");const report = {  worker: visit.workerStart > 0,  waited: visit.workerStart > 0 ? visit.fetchStart - visit.workerStart : 0,  route: visit.workerMatchedSourceType ?? "",  firstByte: visit.responseStart,};navigator.sendBeacon("/metrics", JSON.stringify(report));
  • Line 1 reads the page's navigation entry.
  • Line 3 notes whether a worker took the page at all. Line 4 is the wait for the worker, as in the first example.
  • Line 5 keeps the route that matched, if any. Browsers without it give undefined, so ?? stores an empty string.
  • Line 6 keeps the first byte. Line 8 sends the report with navigator.sendBeacon(), as lesson 24.2 did.

Group the reports by that wait. Visits that waited longer most likely woke the worker. Compare them with each other, and the quick ones with the quick ones. Add the photo time from your LCP reports.

Chrome has two more times for routes: workerRouterEvaluationStart, when it began checking them, and workerCacheLookupStart, when it began looking in the cache. Here is every version, side by side. Read each row across: the version, the wait with the worker asleep, the first byte, then the photo asleep and awake.

Every version of the shop, side by side
Version of the shopWaiting for the workerFirst bytePhoto, worker asleepPhoto, worker awake
No workerNot asked677.6 ms776 ms772 ms
Pass-through worker3.2 ms681.6 ms804 ms776 ms
Network first3.3 ms680.7 ms772 ms772 ms
Network first, with preload3.3 ms682.8 ms776 ms772 ms
Preload, never used3.7 ms1,043.1 ms1,140 ms1,172 ms
Stale-while-revalidate3.4 ms8.5 ms108 ms100 ms
Static routesNot asked677.8 ms768 ms784 ms

Only stale-while-revalidate made the photo come much sooner. Preload, never used, made it much later. Every other version stayed within 30 ms of no worker. Why it matters: measure the photo, not just the worker's wait. Then you'll see a gain only where the visitor saw one.

Here are the same fields for the page you're reading now:

Playground: this page's worker fields
performance.getEntriesByType("navigation")[0]
  1. Nothing read yet.
Try it yourself
Press the button

The lab reads this page's own navigation entry, in your browser, and shows each worker field it has.

Live in your browser. A field your browser doesn't have says so.

Which route for each request?

Sort each request by how the shop's worker should answer it:

Which route for each request?
  • /assets/style.v1.css, a file with its version in its name
  • The mint tea photo, mint-tea.v1.jpg
  • The tea list, where a list from the last visit is fine
  • The reviews under each tea
  • The basket page, which must show what's in it now
  • An order's status
  • The checkout form's POST request
  • The payment page
Try it yourself
0 of 8 correct

Sort each request by how the worker should answer it.

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

Common mix-ups

  • "Any service worker makes return visits faster." No. The pass-through worker showed the photo at 804 ms, against 776 ms with no worker.
  • "Waking a worker always takes hundreds of ms." No. It took about 3 ms on the course's laptop. web.dev puts it at about 250 ms on phones. Measure your own visitors.
  • "Navigation preload is free to switch on." No. If the worker never reads event.preloadResponse, the server builds every page twice. Asha's page came 360 ms later.
  • "Stale-while-revalidate always refreshes the copy." No. With the worker asleep, Chrome dropped the refresh, and the old list stayed for one more visit.
  • "A static route switches the worker off." No. Chrome still woke it in the background. The route only stopped the page from waiting for it.
  • "A workerStart of 0 means the worker was fast." No. It means no worker took the request: there was none, or a route sent the request around it.

Practice exercises

Answer each one first. Then open the hints if you need them.

Exercise 1 · Warm-upA slow phone's wake-up

Here is one made-up visit on a slow phone. What does the last line print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// A made-up return visit on a slow phone, from its navigation entry, in ms.
const visit = { workerStart: 3, fetchStart: 253, responseStart: 931 };

console.log(`Waking took ${visit.fetchStart - visit.workerStart} ms.`);

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

    Exercise 2 · Warm-upPick the strategy

    Ravi's worker picks a strategy for each request. What does it print for the photo?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function strategyFor(request) {
      if (request.mode === "navigate") return "stale-while-revalidate";
      if (request.destination === "image") return "cache first";
      return "network first";
    }
    
    console.log(strategyFor({ mode: "no-cors", destination: "image" }));

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

      Exercise 3 · PracticeThe refresh, with the worker awake

      With the stale-while-revalidate worker awake, how many requests for the page reached the server?

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

        Exercise 4 · PracticeA phone-sized wake-up

        A worker takes 250 ms to wake. With no preload of any kind, when does the first byte come?

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

          Exercise 5 · ChallengeWhere the fetch event ran

          Open the course's trace of a visit with the pass-through worker asleep in Chrome's Performance panel. The worker's fetch event ran on its own track. What is that track called?

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

            Exercise 6 · ChallengeWhere DevTools lists the routes

            Which DevTools panel, and which tab in it, shows the routes a worker registered?

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

              Check your understanding

              For each question, ask two things: did the worker have to wake, and where did the answer come from?

              Service workers and performance quiz · 7 questionsScore: first tries count
              1. Question 1 of 7A worker answers every request with fetch(event.request). What did it do to Asha's return visit?

                Choose an answer to see the explanation.

              2. Question 2 of 7This is one of Asha's visits with the stale-while-revalidate worker asleep. What does it print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const visit = { workerStart: 2, fetchStart: 5.8, responseStart: 8.5 };
                console.log((visit.fetchStart - visit.workerStart).toFixed(1));

                Choose an answer to see the explanation.

              3. Question 3 of 7What does navigation preload change?

                Choose an answer to see the explanation.

              4. Question 4 of 7A worker turns navigation preload on, but its handler still calls fetch(event.request). What happened in the recording?

                Choose an answer to see the explanation.

              5. Question 5 of 7Which version gave the fastest return visit, and what did it cost?

                Choose an answer to see the explanation.

              6. Question 6 of 7With static routes, the page matched the route for when the worker is "not-running". What did its navigation entry say?

                Choose an answer to see the explanation.

              7. Question 7 of 7Why compare visits that woke the worker only with each other?

                Choose an answer to see the explanation.

              Key takeaways

              • A worker makes a return visit faster only when it answers from Cache Storage.
              • A stopped worker must wake before it can answer. fetchStart minus workerStart shows the wait.
              • Navigation preload starts the page's request while the worker wakes. Always use its answer.
              • Answer versioned files cache first. Pick each page's strategy by how fresh it must be.
              • Static routes send requests to the cache or the network without running the worker's code.
              • Compare asleep visits with asleep visits, and measure the photo, not only the worker.

              Remember the one-liner.
              A service worker speeds up a return visit only when it answers from its cache; for everything else, let the network start without it.

              Sources

              The facts on this page come from these specs, docs and source code:

              Coming next: How INP works. Asha's pages now load fast. Next, how fast they answer her taps.

              CompleteFrontend Clear concepts. Working examples.