Service Worker

With the rapid development of the front end, application performance has become critical, and there are a lot of statistics about this. You can check it out.

How to reduce the network request cost of a page so as to shorten the page load resource time and reduce the user perceived delay is very important. The common means to improve the loading speed of applications are Http Cache, asynchronous loading, 304 Cache, file compression, CDN, CSS Sprite, enable GZIP and so on. All of this does is make resources faster to download to the browser. But in addition to these methods, there are more powerful Service Worker threads.

Current status of Service Workers and PWA

Speaking of service worker, WE have to mention PWA. As one of the core technologies of PWA, service worker has been vigorously promoted by Google for many years. Here is a brief introduction.

In plain English, PWA is Progressive Web App. Back in early 2016, Google introduced PWA to provide a more powerful Web experience and lead developers back to the open Internet. It makes up for several features that the Web lacks in comparison to Native apps, such as offline use, background loading, adding to the home screen, and notification push. It also has the “no install, use it and go” feature touted by the miniapp.

Although PWA technology has been approved by W3C as a standard, its adoption has been disappointing and has been blocked by Apple, most importantly because PWA bypassed Apple Store approval and pushed directly to users. If it becomes popular, it could threaten Apple’s platform authority and mean that apple’s 30-70 split with developers would be lost.

So Safrai does not support mainfest and Service worker, two key technologies. Even though safrai started to support them in 2018, its support for PWA is far lower than android, which is reflected in the fact that the service worker cache cannot be permanently saved. And service worker API support is not perfect, one of the most obvious differences is that android VERSION of PWA will keep your login status and system level push messages. With Apple, you can’t do either. In other words, PWA on the iPhone, you have to log in again every time you open it, and you don’t receive any push messages.

In addition, due to some undescribable reasons, Service Worker’s push function cannot be used in China. Although two domestic companies have done browser push of Service Worker, its maturity remains to be investigated. At present, different mobile browsers have different support degrees for Service worker, and the same interface also has differences and needs to be unified. For us, we can only use service worker to do the cache of PC browser.

The origin of Service workers

Service workers (hereinafter referred to as SW) are based on WEB workers.

As we all know, javaScript is single threaded. With the complexity of Web business, developers gradually do a lot of resource-consuming operations in JS, which makes the disadvantages of single threading even more obvious. The Web worker was created on this basis, it is off the main thread, and we can hand over complex and time-consuming tasks to the Web worker. However, as an independent thread, Web worker should have more functions than that. Sw adds the ability of offline cache on the basis of Web worker. Of course, before Service workers, there was an API for offline caching in HTML5 called AppCache, but AppCache has a lot of disadvantages that you can see for yourself.

Sw is event-driven and has a life cycle. It can intercept and process all fetch requests of the page, access cache and indexDB, support push, and allow developers to control and manage the content and version of the cache, making it possible for the web to run in an offline weak network environment. Make the Web experience more native. In other words, it can cache all the static and dynamic resources in your application according to different policies, so that you don’t have to go to the server for the next time you open it, thus reducing the network time, allowing the Web application to start in seconds and become available offline. All you need to do is add a sw file without any intrusion into the original code, isn’t it perfect?

Basic features of Service workers

  • Unable to manipulate DOM

  • Only HTTPS and localhost can be used

  • You can block site-wide requests to control your application
  • Independent of the main thread will no longer block (do not register the SW while the application is loading)
  • Completely asynchronous, unable to use XHR and localStorage
  • Once installed, it exists forever unless it is removed manually by uninstall or dev mode
  • Independent context
  • In response to push
  • Background synchronization…

Service workers are event-driven workers whose life cycle is page-independent. It can also exit when the associated page is not closed, or start when there is no associated page.

The important difference between Dedicated workers, Shared workers and Service workers lies in different life cycles. For Service workers, the document-independent lifecycle is an important foundation for providing reliable Web services.

Service Worker life cycle

  • Register This is initiated by the client to register a serviceWorker, which requires a file that handles the sw logic
  • Parsed is registered and Parsed successfully
  • In the process of installing registration, the INSTALL event will be triggered in the SW, and you need to know that the sw is called in the way of event triggering. If there is event.waitUntil() in the event, it will wait for the incoming Promise to complete before it succeeds
  • Installed (waiting) Registration is complete, but the page is controlled by the old Service Worker script, so the current script is not active and waiting. You can skip the wait by using self.skipWaiting().
  • Activating installation should wait for activation, that is, activated. Install is triggered when register is successful, but activated is not triggered immediately. If the event has event.waitUntil(), the event will wait for the Promise to complete, and then you can call clients.claim () to take over all pages.
  • Activated After that, the SOFTWARE intercepts and processes client requests. The FETCH API is used by the SW, and XHR is unavailable
  • Fetch is activated and starts intercepting requests made in the web page by terminate. This step is the browser’s judgment. When the SW is idle after it has not been used for a long time, the browser will suspend the SW until it is used again
  • The update browser will automatically detect the update of the SW file, and will download and install when there is an update. However, the old SW is still in control of the page, and the new SW can activate the control page only when the user opens a new window
  • Redundant installation fails, or activation fails, or is replaced by a new Service Worker

The most common functions of Service Worker scripts are intercepting requests and caching resource files, which can be tied to the following events:

  • The install event captures resources for caching
  • Activate event, traverses the cache to remove expired resources
  • The FETCH event intercepts the request, queries the cache or network, and returns the requested resource

The Service Worker practice

Before that you can check out Google’s demo

Let’s start with the sw registration. The official demo registration is as follows:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('service-worker.js');
}
Copy the code

However, there are some problems in this way. The page will cache sw resources when it is opened for the first time, because the pre-cached resources in SW need to be downloaded. Once the SW thread downloads resources when it is opened for the first time, it will occupy the bandwidth of the main thread and increase the usage of CPU and memory. It must first apply to the browser UI thread to allocate a thread, and then go back to the IO thread to continue the startup process of the Service worker thread, and then switch between the UI thread and THE IO thread several times. Therefore, there is a certain performance cost during the startup process, especially in the mobile end.

Moreover, the first time to open a variety of resources are very valuable, there is no need to fight for the first time to open the page to cache resources. The correct way to do this is after the page loads the SW.

Proper posture:

if ('serviceWorker' in navigator) {
  window.addEventListener('load'.function() {
    navigator.serviceWorker.register('/sw.js');
  });
}
Copy the code

But is that enough? Only register, so how to log out of the SW when there is a problem? What happens to the cache after logout? These are things to think about in advance

Another important feature of registering with SW is that the fetch requests are listened to differently depending on the scope of SW. If your sw file is located in the root directory of /sw/sw.js, then your SW can only listen for requests under /sw/*. If you want to listen for all requests, you can either put sw.js in the root directory or set scope at registration time.

A simple registration demo with error degradation in mind:

  window.addEventListener('load'.function() {
    const sw = window.navigator.serviceWorker
    const killSW = window.killSW || false
    if(! sw) {return
    }

    if(!!!!!killSW) {
        sw.getRegistration('/serviceWorker'). Then (registration => {// register registration. Unregister (); // Clear the cache window.caches && caches.keys && caches.keys().then()function(keys) {
                keys.forEach(function(key) { caches.delete(key); }); }); })}elseSw.register (sw.register(sw.register(sw.register))'/serviceWorker.js',{scope: '/'}). Then (registration => {// Console.log ('Registered events at scope: ', registration.scope);
        }).catch(err => {
            console.error(err)
        })
    }
  });
Copy the code

The following section is what to do in the sw.js file. After the successful registration step above, we first listen in the sw.js file for the install event thrown after the successful registration.

self.addEventListener('install'.function(e) {
  // ...
})
Copy the code

Normally, all we need to do when we listen for this event is cache all the static files

self.addEventListener('install'.function(event) {
  event.waitUntil(
    caches.open('cache-v1').then(function(cache) {
      return cache.addAll([
        '/'."index.html"."main.css",]); })); })Copy the code

This starts with an event.waitUntil function, which is provided by the Service Worker standard and takes a promise argument and listens for all promises in the function. If any promise is reject, the installation fails. For example, if a resource fails to be downloaded in cache.addAll, the entire installation fails and all subsequent operations are not performed. Another important feature of waitUntil is that it extends the event life cycle. Since the browser is sleeping with the SW at any time, the event. WaitUntil is captured to prevent execution interruptions, and when all loads are successful, the SW can move on.

In addition, the list of cached files here should normally be generated automatically at build time using webPack plug-ins or other tools. The version number of the cache should also be changed independently; here we treat each build as a new version.

If a service worker process already exists on the page, the new sw will not be activated until the next time the page is opened. If a service worker process already exists on the page, the next sw will be activated. Or use self.skipwaiting () to skip the wait.

const cacheStorageKey = 'testCache1';
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      returncacheNames.filter(cacheName => cacheStorageKey ! == cacheName); }).then(cachesToDelete => {return Promise.all(cachesToDelete.map(cacheToDelete => {
        returncaches.delete(cacheToDelete); })); }). Then (() = > {/ / immediately take over all pages self. Clients. The claim ()})); });Copy the code

At activate we usually check and delete the old cache, and if the event has event.waitUntil(), we wait for the Promise to complete. Call clients.claim () to take over all pages. Note that this causes the new version of THE SW to take over the old version of the page.

Once activated, the fetch event intercepts all requests within the scope of the site, giving you the flexibility to use apis such as indexDB and Caches to define your caching rules.

// Make a request to match the cache according to the URI, make a request if the cache does not match, and cache the request self.addeventListener ('fetch'.function(event) {
  event.respondWith(
    caches.match(event.request).then(function(resp) {
      return resp || fetch(event.request).then(function(response) {
        return caches.open('v1').then(function(cache) {
          cache.put(event.request, response.clone());
          returnresponse; }); }); })); });Copy the code

Event.respondwith: Receives a promise parameter and returns the result to the controlled client, which can be any custom response-generating code.

There are also some questions:

  • {credential: ‘include’} {credential: ‘include’}
  • For cross-domain resources, you need to set {mode: ‘cors’}; otherwise, the corresponding data cannot be retrieved in Response
  • For cache requests, the body in Request & Response can be read only once, because the Request and Response streams can be read only once. The body contains the bodyUsed property. When used, the value of this property will be true and cannot be read again. The Request & Response to clone down: Request. Clone () | | Response. The clone ()

Of course, this is just a demo, and it’s impossible to cache all requests like this. If you use tools such as SW-Toolbox to implement SW, there are usually several cache strategies:

  • NetworkFirst: First attempts to process the request over the network, if successful stores the response in the cache, otherwise returns the cached resource to respond to the request. It works for API requests where you always want to return data that is up to date, but if you can’t get the latest data, return an old data that is available.
  • CacheFirst: Returns a resource that matches a network request if it exists in the cache, otherwise attempts to fetch it from the network. At the same time, the cache is updated if the network request is successful. This option applies to resources that do not change often or that have other updating mechanisms.
  • Fastest: Requests resources from both the cache and the network in parallel and responds with whichever data is returned first. This usually means that the cached version responds first. On the one hand, this policy always generates network requests, even if the resource is already cached. On the other hand, when the network request completes, the existing cache is updated so that the cache read next will be up to date.
  • CacheOnly: The request is parsed from the cache and fails if there is no corresponding cache. This option is suitable for situations where you need to ensure that no network request is made, such as saving power on a mobile device.
  • NetworkOnly: Attempts to get a url from the network to process the request. If the resource fails to get, the request fails, which is essentially the same as if the service worker were not used.

Or different policies depending on the request type or file type, or more complex policies:

self.addEventListener('fetch'.function(event) { var request = event.request; // Non-get requestif(request.method ! = ='GET') { event.respondWith( ... ) ;return; } // HTML page requestif (request.headers.get('Accept').indexOf('text/html')! == -1) { event.respondWith( ... ) ;return; } // get interface requestif (request.headers.get('Accept').indexOf('application/json')! == -1) { event.respondWith( ... ) ;return; } event.respondwith (...) {event.respondwith (...); ; }Copy the code

Service Worker updates

The first time a user accesses a website or page controlled by the SW, the SW is downloaded immediately.

It will be downloaded at least every 24 hours after that. It may be downloaded more frequently, but it must be downloaded every 24 hours to prevent bad scripts from running for too long, which is the browser’s own behavior.

The browser compares each downloaded PIECE of SOFTWARE with the existing one, byte by byte, and installs it when it finds a difference. However, the old activated SW is still running, and the new SW will enter the waiting state after installation. Until all open pages are closed, the old sw stops automatically, and the new SW will not take effect in the following reopened pages.

Updates in SW can be divided into two types, basic static resource updates and sw.js file itself updates. Either way, you have to make changes to the SW file, which means you have to install a new SW.

Let’s start with a scenario where the site’s existing SW cache is named v1, i.e. at install we pre-cache caches with caches. Open (‘v1’), where all old resources are stored under v1 in caches.

self.addEventListener('install'.function(e) {
  e.waitUntil(
    caches.open('v1').then(function(cache) {
      return cache.addAll([
       "index.html"])}})))Copy the code

Now that the site is updated, we can simply rename v1 in chache to v2. When we change the sw file, the browser will automatically update the sw.js file and trigger the install event to download the latest file (update cache can happen anywhere), and the new site will be stored in the V2 cache. After the new SW is activated, v2 caching is enabled.

This is a simple and safe way to do it, and it’s a natural obsolescence of the old version, but closing all pages is a user choice, not a programmer’s control. One other thing to note is that because of the browser’s internal implementation, when a page switches or refreshes itself, the browser waits until the new page is rendered before destroying the old one. This means that there is a crossover time between the old and new pages, so simply switching pages or refreshing won’t cause the sw to update, the old SW will still take over the page and the new SW will still be waiting. In other words, even if the user knows that your site has been updated, and the user does f5 on the browser side, the user will still see the old version of the page because the old SW is still alive. So how can we get the new SW to take over pages as soon as possible?

That is to use the self.skipWaiting() method inside the sw.

self.addEventListener('install'.function(e) {
  e.waitUntil(
    caches.open(cacheStorageKey).then(function(cache) {
      return cache.addAll(cacheList)
    }).then(function() {// The wait will be skipped if the registration succeedsreturn self.skipWaiting()
    })
  )
})
Copy the code

But obviously, on the same page, the first half of the request is controlled by the old SW and the second half is controlled by the new SW. Inconsistency between the two can easily lead to problems, unless you can ensure that the same page will work in both versions of the SOFTWARE.

That said, it’s best to ensure that the page is handled by a SINGLE SW from beginning to end, which is pretty simple:

navigator.serviceWorker.addEventListener('controllerchange', () => {
  window.location.reload();
})
Copy the code

We can listen for the ControllerChange event where the sw is registered to see if the sw that controls the current page has changed, and then refresh the site so that we are controlled by the new SW from start to finish to avoid the problem of old and new sw switching. However, the sw change happened within a few seconds of loading the page, and when users opened the site, they encountered a strange refresh. If you don’t want to be insulted by users, we will consider a better way.

Refreshing a page without warning is unacceptable. Let’s see what Baidu’s Lavas framework does

When they tested the new sw have been installed pop-up a prompt to tell the user site has been updated, and allow the user to click the update button, the notification bar is very simple (ugly) however lavas, practical application we can above rich content, such as increasing update log or something like that, in addition the button is not outstanding, For many times I thought I was doing the same thing by pressing F5, until I understood how it worked and realized that I could only switch between old and new SW by clicking this button.

The onUpdatefound method is triggered when the new sw is installed and listens to this method to pop up a prompt for the user to click the button.

navigator.serviceWorker.register('/service-worker.js').then(function(reg) {// Registration. Waiting will return the state of the installed sw with an initial value of null. The onUpdatefound event will not happen again // see https://github.com/lavas-project/lavas/issues/212 for detailsif(reg.waiting) {// The notification prompt is displayedreturn; } // This method reg.onUpdatefound = is called every time the registration.installing property gets a new SWfunction () {
     const installingWorker = reg.installing;
     // 
     installingWorker.onstatechange = function () {
       switch (installingWorker.state) {
         case 'installed'Onupdatefound is also called when the sw is installed for the first time, so check to see if it is controlled by the SWif(the navigator. ServiceWorker. Controller)} {/ / notification bar displaybreak; }}; }; }).catch(function(e) {
   console.error('Error during service worker registration:', e);
 });
Copy the code

The next step is to deal with what happens after the notification click event. Here, only the interaction with the SW is written, and a message is sent to the waiting SW.

try {
  navigator.serviceWorker.getRegistration().then(reg => {
    reg.waiting.postMessage('skipWaiting');
  });
} catch (e) {
  window.location.reload();
}
Copy the code

When the SW receives the message, it performs the skip wait operation.

// service-worker.js // SW no longer executes skipWaiting in install phase self.addEventListener('message', event => {
  if (event.data === 'skipWaiting') { self.skipWaiting(); }})Copy the code

The next step is to perform the refresh by listening for the ControllerChange event through navigator.serviceWorker. Well, that solves the problem, but it can only be updated by clicking the update button, not by the user refreshing the browser.

Complete demo

Library Service Worker

Google had two PWA wheels in its early days:

  • sw-precache
  • sw-toolbox

Both have webPack plugins, but please note that they have been out of service since 2016 due to the better GoogleChrome/ WorkBox, which Google officially recommends as workbox, and Baidu’s Lavas, which now uses the same wheel.

There is also a NekR/offline-plugin that also supports AppCache.



Worktile’s website: Worktile.com

Author: Yan Dong

This article was first published on Worktile’s official blog.