DEV Community

Demián Renzulli
Demián Renzulli

Posted on • Edited on

Prefetching techniques with Workbox

Overview

Prefetching is a great way of improving load times for future navigations. The most common way of implementing them is by adding <link rel=prefetch> tags on the header of a page indicating the resource to prefetch.
Prefetched resources are requested at the Lowest priority and kept for 5 minutes in the prefetch cache, after which point the cache-control headers of the resource apply. If you are not familiar with traditional prefetching, you can check this article.
Service workers offer additional caching capabilities that can be used to complement prefetching. In this guide, we’ll explore different ways of leveraging this, by using a popular library to build service workers: Workbox.

Prefetching with service workers

Service workers can be used in prefetching scenarios mostly in two ways:

- As a complement of traditional prefetching: In most cases you'd want to delegate to the browser the decision of when and how to prefetch resources, by using <link rel=prefetch> tags. In these cases, the service workers can be used as a complement, for example, to extend the lifetime of the prefetched resources.

- As a replacement of traditional prefetching: A page can communicate with the controlling service worker to delegate it the task of prefetching, instead of using resource hints. The service worker can also prefetch files at certain events of its lifecyle, without being asked explicitly by the page to do so (e.g. at install time).

In this guide, we'll review the pros and cons of each approach.
The following table summarizes the different patterns we'll explore. The first two strategies are meant to be a complement of traditional prefetching, while the last two are ways of prefetching directly in the service worker, without using resource hints:

Strategy When to use it Tool
Runtime caching To extend the lifetime of the resources prefetched with <link rel=prefetch> tags Workbox runtime caching
Precaching static assets To complement <link rel=prefetch> used for pages, by pre-caching also the page's subresources Workbox precaching
Precaching documents To prefetch static pages at SW install time Workbox precaching
Window <-> SW communication To delegate prefetching to the SW instead of using <link rel=prefetch> tags Workbox window

Note: while some of these patterns can work for single page applications, that kind of architecture can present some challenges in prefetching scenarios. For that reason, in this guide we’ll mostly focus on use cases for multi-page apps.

Precaching

Precaching is the ability to save a set of files to the cache when the service worker is installing. It’s commonly used to caching content ahead of the service worker being used.

We’ll explore two ways of leveraging precaching in prefetch scenarios:

  • Precaching static pages: Storing documents in the SW cache at install time, instead of implementing prefetch tags for these pages throughout the site.
  • Precaching page subresources: Storing page subresources (like JS, CSS, etc) that pages will likely request in the future. This can be a general PWA practice, but can give an extra boost in prefetching scenarios.

Precaching documents

Some pages are completely generated at build time. This is common in static sites, and also in websites that have a mix of dynamic and static pages (e.g. about.html, contact.html, etc).
Instead of using <link rel=prefetch> tags throughout the site for these static pages, you can rely on the SW to do this in the background, at install time.

In Workbox you can precache static pages with code like the following:

workbox.precaching.precacheAndRoute([
  { url: '/about.html', revision: 'abcd1234' },
  // ... other entries ...
]);

Using Workbox in this way, gives the additional benefit of keeping these files up-to-date whenever they change.
In the previous code the revision property is an auto-generated hash of the file contents. When the page changes and a new build is ran, the revision property will change, generating a new SW that will manage cache updates when pushed to the client.

Prefetching page subresources

In the previous section we explored how to precache static documents with Workbox. The same approach can be used to cache static resources the different sections of the site might request at some point (e.g. JS, CSS, etc). This can be a general PWA practice, but can make navigations even faster in prefetching scenarios.

For example: in a product listing page you could use <link rel=prefetch> tags to request the first few items a product listing, so that, when the user clicks on a product, the document will be available in the cache. If you had the page subresources already precached, you could make load these navigations even faster.

This technique consists on the following steps:

1: Implement <link rel=prefetch> tags for product items in the listing page:

 <link rel="prefetch" href="/phones/smartphone-5x.html" as="document">

2: Precache the page's subresources in the service worker:

workbox.precaching.precacheAndRoute([
  '/styles/product-page.ac29.css'
  // ... other entries ...
]);

Note: this was a very quick overview of precaching. For an in-depth overview, make sure to check the precaching with Workbox guide.

Runtime Caching

Runtime caching refers to gradually adding responses to a cache "as you go" in order to make future requests for the same URL more reliable.

By definition, <link rel=prefetch> is a way of of runtime caching (without a service worker). As mentioned earlier, when using <link rel=prefetch> tags, resources are kept for 5 minutes in the prefetch cache, after which point the cache-control rules of the resources apply.

Service wrokers allow to extend the lifetime of the prefetch resource, beyond the limits of traditional prefetching. Additionally they can respond when user is navigating offline or in flaky connections.

In our example of a listing page, you could implement this technique in the following way:

1: Add <link rel=prefetch> tags for product items in the listing page:

 <link rel="prefetch" href="/phones/smartphone-5x.html" as="document">

2: Implement a runtime caching strategy in a service worker, with a regular expression to matches requests for html pages:

new workbox.strategies.StaleWhileRevalidate({
   cacheName: 'document-cache',
   plugins: [
      new workbox.expiration.Plugin({
      maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
      })
   ]
   })
);

Workbox provides different runtime caching strategies that can be used in cases like these. In the previous example, we opted for workbox.strategies.StaleWhileRevalidate(), since it’s a good choice for documents.
When using this strategy, resources are requested from both the cache and the network, in parallel. The response will come from the cache if available, otherwise from the network. Regardless of this, the cache is always updated with the network response with each successful request.

Window <-> SW communication

<link rel=prefetch> is designed to make prefetching as efficient as possible, by delegating the decision of prefetching or not to the browser. The request will take place in the browser’s idle time, and will be sent at the lowest priority. In most cases, you’d want to rely on this native browser mechanism to achieve prefetching, instead of re-inventing the wheel.

In some cases, though, you might prefer to delegate this task to the service worker. For example: if you have a client-side rendered product listing page, you might need to inject several <link rel=prefetch> dynamically in the page, based on an API response, to prefetch the first products of the listing. This is not necessarily an inefficient technique, but can momentarily consume time on the page’s main thread.
As the code in the SW runs on its own separate thread, offloading this work to the SW could be considered a viable option in cases like these.

You can implement prefetching directly in the service worker in the following ways:

- Using a Runtime caching strategy: Intercepting a request for an API call made by the page, parsing the response of the API, and issuing an additional number of requests to cache in the service worker for links contained in the response.

- Implementing a “window to service worker” communication strategy: Either by using postMessage(), or by leveraging a Workbox module called workbox-window.

Since we've already covered Runtime Caching strategies in the previous section, we’ll devote the rest of this section to the second technique.

Prefetching with workbox-window

A page can communicate with a service worker, by using postMessage(), and pass it a list of URLs to prefetch. This code sample shows how to implement this kind of page to SW communication pattern.

Workbox has a package called workbox-window that abstracts some details of the window to service worker communication process, making it easier to implement.

Here are the steps to implement a prefetching strategy with workbox-window:

1: Call the service worker from the page, passing it the type of message, and a URL or list of URLs to prefetch. Alternatively, you can create an API that returns the URLs to prefetch, and pass it to the service worker so it can request the list of URLs, instead of receiving a URL array as parameter:

const wb = new Workbox('/sw.js');
wb.register();

const prefetchResponse = await wb.messageSW({type: 'PREFETCH_URLS', urls: [...]});

2: Intercept the message sent by the page in the service worker, then send a request for each URL to prefetch, and store the each response in the cache:

addEventListener('message', (event) => {
  if (event.data.type === 'PREFETCH_URLS') {
    // Fetch URLs and store them in the cache
  }
});

Conclusion

In this guide, we have reviewed different strategies to combine prefetching techniques with service workers.
In most cases, you’d want to use the traditional resource hint <link rel=prefetch>, and use service worker caching as a complement to the default browser caching behavior.
In other scenarios, you might prefer to delegate the work to the service worker, either by precaching static pages (at service worker install time), or by implementing a page to service worker communication strategy (e.g: postMessage(), or using workbox-window).

Keep in mind that prefetching can greatly improve peformance of navigations, but it comes at a cost. In all these cases, requesting additional files will consume extra bytes for resources that are not immediately needed. Make sure to apply these techniques thoughtfully: only prefetch resources when you are confident that users will need them.

With many for their feedback and reviews to Addy Osmani, Jeff Posnick, Phillip Walton, Gilberto Cocchi and Jonathan Chen.

Top comments (1)

Collapse
 
sefrem profile image
sefrem

Thank you for the article. It was a useful comparing of sw caching techniques and in browser ones. One question. You, as well as google documentation on prefetching mention "In the previous code the revision property is an auto-generated hash of the file contents." How can i do that? Auto-generate hash i mean. Where can i read more about that?