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.
- 01Measure what a worker costsRead fetchStart minus workerStart, and tell a visit that woke the worker from one that found it awake.
- 02Keep the network from waitingTurn on navigation preload and use its answer, and send requests around the worker with static routes.
- 03Pick 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:
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, 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.
firstByteis when the first byte of the page arrived.photois 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 msandA 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.
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
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:
workerStartis 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.fetchStartcomes 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:
// 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
workerStartfromfetchStart. - Lines 6 and 7 print
Worker asleep: the request waited 3.2 msandWorker 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:
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:
- AsleepAwake
- Waiting for the worker3.2 ms0.1 ms
- First byte of the page681.6 ms679.8 ms
- Photo on screen804 ms776 ms
- Where the page came fromThe networkThe network
- Requests for the page at the server11
- Teas on the list99
Asha's return visits with this version: on the left with the worker stopped first, on the right with it still running.
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.
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:
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:
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:
- No worker678.8 ms
- Network first, no preload of any kind679.4 ms
- Network first, with navigation preload680.8 ms
- Stale-while-revalidate, from the cache2.4 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.
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 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.
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.
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:
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.
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.
| Strategy | Where Asha's answer comes from | How fresh it is | Good for |
|---|---|---|---|
| Cache first | Cache 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 first | The 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-revalidate | Cache 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 only | The 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.
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:
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 namedassets. 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:
// 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.
workerMatchedSourceTypesays which route's source matched: the network.workerStartis 0, because the worker never took the request. - Line 3 is the photo. It matched the cache route, and
deliveryTypesays it came from Cache Storage. - Lines 5 and 6 print
The page: network, workerStart 0andThe photo: cache, from cache-storage.
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.
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:
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.
| Version of the shop | Waiting for the worker | First byte | Photo, worker asleep | Photo, worker awake |
|---|---|---|---|---|
| No worker | Not asked | 677.6 ms | 776 ms | 772 ms |
| Pass-through worker | 3.2 ms | 681.6 ms | 804 ms | 776 ms |
| Network first | 3.3 ms | 680.7 ms | 772 ms | 772 ms |
| Network first, with preload | 3.3 ms | 682.8 ms | 776 ms | 772 ms |
| Preload, never used | 3.7 ms | 1,043.1 ms | 1,140 ms | 1,172 ms |
| Stale-while-revalidate | 3.4 ms | 8.5 ms | 108 ms | 100 ms |
| Static routes | Not asked | 677.8 ms | 768 ms | 784 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:
- Nothing read yet.
The lab reads this page's own navigation entry, in your browser, and shows each worker field it has.
Which route for each request?
Sort each request by how the shop's worker should answer it:
/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
Sort each request by how the worker should answer it.
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.
Here is one made-up visit on a slow phone. What does the last line print?
// 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.`);Waking took 250 ms. That's web.dev's typical phone. The page's request waited all of it, unless the browser started it early.
Ravi's worker picks a strategy for each request. What does it print for the photo?
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" }));cache first. A photo isn't a page, so line 2 is skipped, and line 3 matches its destination.
With the stale-while-revalidate worker awake, how many requests for the page reached the server?
1. With the worker awake, Chrome didn't start the request itself, so the worker's own refresh reached the server. The next visit would show 9 teas.
A worker takes 250 ms to wake. With no preload of any kind, when does the first byte come?
929.4 ms: the recorded 679.4 ms plus the 250 ms wake-up. With navigation preload, the lab stays at 680.8 ms, because the request leaves at once.
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?
ServiceWorker thread. Chrome started the worker in the page's own renderer process, but on a thread of its own, so it didn't use the page's main thread.
Which DevTools panel, and which tab in it, shows the routes a worker registered?
In the Application panel, on the Service workers tab. The Network panel also marks each request that matched a route, in its Size column.
Check your understanding
For each question, ask two things: did the worker have to wake, and where did the answer come from?
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.
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 predictconst visit = { workerStart: 2, fetchStart: 5.8, responseStart: 8.5 }; console.log((visit.fetchStart - visit.workerStart).toFixed(1));Choose an answer to see the explanation.
Question 3 of 7What does navigation preload change?
Choose an answer to see the explanation.
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.
Question 5 of 7Which version gave the fastest return visit, and what did it cost?
Choose an answer to see the explanation.
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.
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:
- Service Workers (editor's draft), W3C.
- PerformanceResourceTiming: workerStart property, MDN.
- performance_resource_timing.cc: with a worker, fetchStart is when it was ready (Chromium 157.0.8103.52), Chromium source.
- inspector_emulation_agent.cc: CPU throttling is only for pages, not workers (Chromium 157.0.8103.52), Chromium source.
- Speed up service worker with navigation preloads, web.dev.
- Optional browser optimization for Service Workers, Chrome Platform Status.
- content_features.cc: ServiceWorkerAutoPreload is on by default (Chromium 157.0.8103.52), Chromium source.
- service_worker_main_resource_loader.cc: navigation preload comes first, and auto preload only wakes a stopped worker (Chromium 157.0.8103.52), Chromium source.
- Explainer for ServiceWorkerAutoPreload, the Chrome loading team.
- Navigation preload for network-first HTML, Chrome for Developers.
- Strategies for service worker caching, Chrome for Developers.
- Service worker caching and HTTP caching, web.dev.
- Use the Service Worker Static Routing API to bypass the service worker for specific paths, Chrome for Developers.
- InstallEvent: addRoutes() method, MDN.
- service_worker_main_resource_loader.cc: a route to the network still starts the worker, off the critical path (Chromium 157.0.8103.52), Chromium source.
- Resource Timing, W3C.
- Timing information for ServiceWorker Static Routing API (Chromium 157.0.8103.52), Chromium docs.
- Browser compatibility data 8.1.8, MDN.
Coming next: How INP works. Asha's pages now load fast. Next, how fast they answer her taps.