Speed up service worker with navigation preloads
Summary
- In some situations, service worker boot-up time can delay a network response.
- Available in the three major browser engines, navigation preload fixes this by allowing you to make the request in parallel with service worker boot-up.
- You can distinguish preload requests from regular navigations using a header, and serve different content.
The problem
The bootup time depends on the device and conditions. It's usually around 50ms. On mobile it's more like 250ms. In extreme cases (slow devices, CPU in distress) it can be over 500ms. However, since the service worker stays awake for a browser-determined time between events, you only get this delay occasionally, such as when the user navigates to your site from a fresh tab, or another site.
The boot-up time isn't a problem if you're responding from the cache, as the benefit of skipping the network is greater than the boot-up delay. But if you're responding using the network…
The network request is delayed by the service worker booting-up.
We're continuing to reduce the boot-up time by using code-caching in V8, by skipping service workers that don't have a fetch event, by launching service workers speculatively, and other optimizations. However, bootup time will always be greater than zero.
Facebook brought the impact of this problem to our attention, and asked for a way to perform navigation requests in parallel:
And we said "yeah, seems fair".
"Navigation preload" to the rescue {: #the-solution }
The startup delay is still there, but it doesn't block the network request, so the user gets content sooner.
Here's a video of it in action, where the service worker is given a deliberate 500ms startup delay using a while-loop:
https://www.youtube.com/watch?v=icv_DpQLryE
Here's the demo itself. To get the benefits of navigation preload, you'll need a browser that supports it.
Activating navigation preload
addEventListener('activate', event => {
event.waitUntil(async function() {
// Feature-detect
if (self.registration.navigationPreload) {
// Enable navigation preloads!
await self.registration.navigationPreload.enable();
}
}());
});
Using the preloaded response
Now the browser will be performing preloads for navigations, but you still need to use the response:
addEventListener('fetch', event => {
event.respondWith(async function() {
// Respond from the cache if we can
const cachedResponse = await caches.match(event.request);
if (cachedResponse) return cachedResponse;
// Else, use the preloaded response, if it's there
const response = await event.preloadResponse;
if (response) return response;
// Else try the network.
return fetch(event.request);
}());
});
event.preloadResponse is a promise that resolves with a response, if:
Otherwise event.preloadResponse is still there, but it resolves with undefined.
Custom responses for preloads {: #header }
If your page needs data from the network, the quickest way is to request it in the service worker and create a single streamed response containing parts from the cache and parts from the network.
Say we wanted to display an article:
addEventListener('fetch', event => {
const url = new URL(event.request.url);
const includeURL = new URL(url);
includeURL.pathname += 'include';
if (isArticleURL(url)) {
event.respondWith(async function() {
// We're going to build a single request from multiple parts.
const parts = [
// The top of the page.
caches.match('/article-top.include'),
// The primary content
fetch(includeURL)
// A fallback if the network fails.
.catch(() => caches.match('/article-offline.include')),
// The bottom of the page
caches.match('/article-bottom.include')
];
// Merge them all together.
const {done, response} = await mergeResponses(parts);
// Wait until the stream is complete.
event.waitUntil(done);
// Return the merged response.
return response;
}());
}
});
In the above, mergeResponses is a little function that merges the streams of each request. This means we can display the cached header while the network content streams in.
This is quicker than the "app shell" model as the network request is made along with the page request, and the content can stream without major hacks.
However, the request for includeURL will be delayed by the service worker's startup time. We can use navigation preload to fix this too, but in this case we don't want to preload the full page, we want to preload an include.
To support this, a header is sent with every preload request:
Service-Worker-Navigation-Preload: true
The server can use this to send different content for navigation preload requests than it would for a regular navigation request. Just remember to add a Vary: Service-Worker-Navigation-Preload header, so caches know that your responses differ.
Now we can use the preload request:
// Try to use the preload
const networkContent = Promise.resolve(event.preloadResponse)
// Else do a normal fetch
.then(r => r || fetch(includeURL))
// A fallback if the network fails.
.catch(() => caches.match('/article-offline.include'));
const parts = [
caches.match('/article-top.include'),
networkContent,
caches.match('/article-bottom')
];
Promise.resolve(event.preloadResponse) means we get a promise for undefined if event.preloadResponse is undefined. It's a good way to normalize behavior with browsers that don't support event.preloadResponse.
Changing the header
By default, the value of the Service-Worker-Navigation-Preload header is true, but you can set it to whatever you want:
navigator.serviceWorker.ready.then(registration => {
return registration.navigationPreload.setHeaderValue(newValue);
}).then(() => {
console.log('Done!');
});
You could, for example, set it to the ID of the last post you have cached locally, so the server only returns newer data.
Getting the state
You can look up the state of navigation preload using getState:
navigator.serviceWorker.ready.then(registration => {
return registration.navigationPreload.getState();
}).then(state => {
console.log(state.enabled); // boolean
console.log(state.headerValue); // string
});
Many thanks to Matt Falkenhagen and Tsuyoshi Horo for their work on this feature, and help with this article. And a huge thanks to everyone involved in the standardization effort
谢谢你的反馈!