jameslk
jameslk

Reputation: 4828

How do I load a service worker before all other requests?

I'm trying to load a service worker before all subresource requests on the page so I can apply some optimizations to the way subresources are loaded (e.g. lazy-loading, loading minified versions of assets instead of full assets). However, I cannot find a way load my SW before other subresource requests begin.

I created a simple proof-of-concept of what I'm trying to do to 401 any requests handled by my Service Worker (just to make it easier to find when my SW begins handling requests).

Here's my HTML:

<!doctype html>
<head>
    <script>
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('/dl-wps-sw.js', { scope: '/' }).then(function (registration) {
                console.log('Service Worker registration successful with scope: ', registration.scope);
            }, function (err) {
                console.error(err);
            });
        }
    </script>
    ...

and here's my Service Worker:

self.addEventListener('install', function (event) {
    self.skipWaiting();
});

self.addEventListener('activate', () => {
    self.clients.claim();
});

self.addEventListener('fetch', (event) => {
    const init = {status: 401, statusText: 'Blocked!'};
    event.respondWith(new Response(null, init));
});

This is what happens in my browser: enter image description here

As you can see in the screenshot, even though my code to register the Service Worker is at the very top of the page, it doesn't activate and begin handling requests until a bit later, and by then a large number of critical requests I need to catch have already fired.

I found someone else who seemed to be trying to do what I'm trying to accomplish (2 years earlier) in a Github issue for the Service Worker spec: https://github.com/w3c/ServiceWorker/issues/1282

It seems they suggested using a <link rel="serviceworker"... tag to do this, but it appears this link type has since been removed from Chrome for some reason: https://www.chromestatus.com/feature/5682681044008960

I've tried several other ideas to attempt to load my SW first:

Any ideas or strategies I'm missing? My Google-fu has failed me on coming up with a solution.

Upvotes: 8

Views: 4604

Answers (2)

Neil Rackett
Neil Rackett

Reputation: 187

While the default behaviour for a new service worker is to wait until the next page load to take over from the previous one, a service worker can in fact begin controlling existing pages immediately using the Clients.claim() method.

So, by including something like this in your service worker, you should be able to avoid the need to refresh:

self.addEventListener('activate', event => {
  event.waitUntil(clients.claim());
});

self.addEventListener('install', event => {
  self.skipWaiting();
});

You can read more about Clients.claim() on MDN.

Upvotes: 2

anthumchris
anthumchris

Reputation: 9072

You'll need to install the SW alone first, then refresh the page. The SW can then serve content for a SW-enabled page and intercept all other requests. Example: fetch-progress.anthum.com

index.html

<p>Installing Service Worker, please wait...</p>

<script>
  navigator.serviceWorker.register('sw.js')
  .then(reg => {
    if (reg.installing) {
      const sw = reg.installing || reg.waiting;
      sw.onstatechange = function() {
        if (sw.state === 'installed') {
          // SW installed.  Refresh page so SW can respond with SW-enabled page.
          window.location.reload();
        }
      };
    } else if (reg.active) {
      // something's not right or SW is bypassed.  previously-installed SW should have redirected this request to different page
      handleError(new Error('Service Worker is installed and not redirecting.'))
    }
  })
  .catch(handleError)

  function handleError(error) {}
</script>

sw.js

self.addEventListener('fetch', event => {
  const url = event.request.url;
  const scope = self.registration.scope;

  // serve index.html with service-worker-enabled page
  if (url === scope || url === scope+'index.html') {
    const newUrl = scope+'index-sw-enabled.html';
    event.respondWith(fetch(newUrl))
  } else {
    // process other files here
  }
});

index-sw-enabled.html

<!-- 
  This page shows after SW installs.
  Put your main app content on this page.
-->

Upvotes: 8

Related Questions