Service worker as fallback to the prerender resource hint

Posted: Jun 5, 2017

A few months ago I wrote an article about prerendering pages in browsers. The idea looked quite simple. There were a few technologies (Elixir and Neo4j) which I wanted to try. So, I started implementing the solution as a project. Currently, the project supports more features than it was described in the article. But, there is a lack in supporting the prerender resource hint by some browsers. Mostly, I was interested in solving the support issue for Firefox. IE11, Chrome, Edge do support this resource hint. But, recently, there've been come bad news, the chromium team is going to remove the prerender hint. As a result, I needed to find fallback for Firefox and future versions of Chrome/Chromium.

Solution

The first thought was to use the prefetch resource hint.

<link rel="prefetch" href="/list">

Using this hint we can prefetch up to 10 resources, whereas, the prerender hint allows prerendering of one page per window. The prefetch hint looks interesting, but there are limitations here:

The second thought was to use a service worker. I heard people talking of it, but I haven't played with it before. This challenge seemed to be a perfect use case to try Service Workers.

I got following requirements to my service worker:

The last one is very important, the project I am building can be added to dynamic pages as well, thus, it is better to re-fetch a page rather than show the stale one.

First of all, I created a script which requests the cache storage to fetch and cache the predicted page:

// sirko.js

caches.open('sirko-pages')
  .then(function(cache) {
    cache.add(pagePath);
  });

This code gets called once the backend predicts the next page and it is kept outside of the service worker. The cache storage isn't coupled to service workers, it can be used in usual scripts too.

Then I wrote the service worker which catches the fetch event (this event is fired for all outgoing requests from a page):

// sirko_sw.js

self.addEventListener('install', function(event) {
   // activate once the worker gets installed,
   // kick out any previous version of the worker
  event.waitUntil(self.skipWaiting());
});

self.addEventListener('activate', function(event) {
  // immediately take control over pages
  event.waitUntil(self.clients.claim());
});

self.addEventListener('fetch', function(event) {
  var request = event.request;

  var prom = caches.match(
    request, { cacheName: 'sirko-pages' }
  ).then(function(cachedResp) {
    if (cachedResp) return cachedResp;

    return fetch(request);
  });

  event.respondWith(prom);
});

When the cache storage is asked to fetch a page, it keeps the cached response under the corresponding request. To find the cached page, we take the request from the event and ask the cache storage to find the response for it. If it is there, the worker serves the response, hence, the browser doesn't make any request to the backend. Otherwise, it fetches the page and serves it.

The final part of the solution is registration of the service worker:

// sirko.js

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('sirko_sw.js');
}

The service worker is served from the root of a site. There are concerns regarding security, for example, service workers only work over HTTPS (besides the localhost), also, there are scopes which limits them. Therefore, to make them work on each page, we need to serve the service worker from the root:

https://example.org/sirko_sw.js

As I mentioned above, it is important to serve "fresh" pages. Therefore, the following code is added right after the code registering the worker:

// sirko.js

caches.delete('sirko-pages');

If the user uses 2 tabs, only one tab will benefit by the prefetched page. It is fine. Otherwise, if the user changed something in the first one, the stale page would be served after navigating in the second tab.

That is it. The fallback is already added to the sirko client and you can play with it here.

Btw, if you use Chrome 58, the solution won't work. Chrome 58 still tells that the prerender hint is supported:

var link = document.createElement('link');
link.relList.supports('prerender'); // true

It might be supported, but it doesn't work. If you want to try the demo, please, use Firefox.

What do we get?

The prerender hint still works in IE11, Edge and Chrome prior to the 58th version. Having created this simple fallback, we covered Firefox and the future version of Chrome (hopefully, they will fix the issue with the support flag soon). The fallback doesn't provide the instance load, but it improves the page load. Btw, I am going to add a new feature which will prefetch assets along with a page. It will give even better experience to users.

There are a few links which might help you to start working with service workers:

The Firefox team says that it is an experimental thing, hence, the solution might stop working at some point.