Maxim Salnikov

@webmaxru

Service worker:

Taking the best from the past experience and looking to the future

How to use what service worker already can do​

And what else will it learn?

Maxim Salnikov

  • Meetups: Mobile Meetup Oslo, Angular Oslo, Framsia, PWA Oslo

  • Conferences: ngVikings and Mobile Era

  • Technical speaker: Web Platform, PWA

Azure Developer Relations Lead at Microsoft

Predictable caching

Postpone networking while offline

Receiving and showing notifications

Service Worker API

Is there anything REALLY new?

Adding payment methods JIT

Full-scale offline mode

Networking optimizations

install, activate, fetch, backgroundfetchsuccess, backgroundfetchfail, backgroundfetchclick
sync
push, notificationclick
paymentrequest

Logically

Physically

-file(s)

App

Service worker

Browser/OS

Event-driven worker

Cache

fetch
push
sync

Own service worker

self.addEventListener('install', event => {
    // Use Cache API to cache html/js/css
})

self.addEventListener('activate', event => {
    // Clean the cache from the obsolete versions
})

self.addEventListener('fetch', event => {
    // Serve assets from cache or network
})

handmade-service-worker.js

It's only partially a joke

Because...

Redirects?

Fallbacks?

Opaque response?

Versioning?

Cache invalidation?

Spec updates?

Cache storage space?

Variable asset names?

Feature detection?

Minimal required cache update?

Caching strategies?

Routing?

Fine-grained settings?

Kill switch?

I see the old version!!!

Similar to SharedWorker

  • Works in its own global context

  • Works in a separate thread

  • Isn’t tied to a particular page

  • Has no DOM access

Different from SharedWorker

  • Can run without any page at all

  • Works only with HTTPS (localhost is an exception)

  • Can be terminated by the browser anytime

  • Has specified lifecycle model

Lifecycle

'install'

Parsed

Installing

Activating

Redundant

'activate'

Waiting

Active

Service Worker

After all, what is PWA?

Progressive web apps use modern web APIs along with traditional progressive enhancement strategy to create cross-platform web applications.

These apps work everywhere and provide several features that give them the same user experience advantages as native apps.

works everywhere*

* but not everything**

natively

** use progressive enhancement strategy

Installation as a native app

Let's build an App Shell

My App

  • Define the set of assets required to show the minimum viable UI

New version is available.

Reload page?

Service worker

  • install: put the assets into Cache Storage

  • activate: clear Cache Storage from the previous app version assets

  • fetch: if the asset is in Cache Storage serve it from there. Otherwise — download and serve it (and cache it)

Build time

  • Register service worker and listen to its lifecycle events (updates in particular)

Website/webapp

Progressive enhancement

Go for the feature detection

TIP #1

Support: your current browser

PWA Feature Detector

Support: detailed

Web API Confluence

Registration

if ('serviceWorker' in navigator) {

    // Registering service worker

}

Background syncronization

if ('SyncManager' in window) {

    // Implement offline-ready network features

}

Push subscription

if (!('PushManager' in window)) {

    // Hide UI for Web Push subscription

}

Actions in notifications

if ('actions' in Notification.prototype) {

  // We can use different actions

}

Proper time to register

The later the better

TIP #2

Improve

Don't interfere

Don't break

if ('serviceWorker' in navigator) {

        navigator.serviceWorker.register('/sw-workbox.js')
            .then(...);

}
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw-workbox.js')
            .then(...);
    });
}

Inconvenient Truth #1

  • The service worker will not improve the first-load experience

  • On the first load, the user agent downloads resources from the Application Shell set twice

  • In some cases, the service worker will slow down even return visits

Pre-caching

Trust your assets and be intolerant to the outdated ones

TIP #3

Know your toolset

  • Service Worker API

  • Cache API

  • IndexedDB

  • Fetch

  • Clients API

  • Broadcast Channel API

  • Push API

  • Notifications API

  • Local Storage

  • Session Storage

  • XMLHttpRequest

  • DOM

const appShellFilesToCache = [
  ...
  './non-existing.html'
]

sw-handmade.js

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

  event.waitUntil(
    caches.open('appshell').then((cache) => {
      return cache.addAll(appShellFilesToCache)
    })
  )

})
  • HTTP errors

  • Service worker execution time

  • Storage errors

Chrome <6% of free space
Firefox <10% of free space
Safari <50MB
IE10 <250MB
Edge Dependent on volume size

Storage is not unlimited

if ('storage' in navigator && 'estimate' in navigator.storage) {
  navigator.storage.estimate().then(({usage, quota}) => {
    console.log(`Using ${usage} out of ${quota} bytes.`);
  });
}
const appShellFilesToCache = [
  './styles.css',
  ...
  './styles.css'
]

Duplicate resources in addAll()

event.waitUntil(
  caches
    .open('appshell').then(cache => {
      return cache.addAll(['./bad-res.html'])
        .catch(err => {
          console.log(err)

        })
    })
);

Errors handling

event.waitUntil(
  caches
    .open('appshell').then(cache => {
      return cache.addAll(['./bad-res.html'])
        .catch(err => {
          console.log(err)
          throw err
        })
    })
);

Errors handling

Caching from other origins

Get ready for opaque

TIP #4

Opaque responses limitations

  • The status property of an opaque response is always set to 0, regardless of whether the original request succeeded or failed

  • The Cache API's add()/addAll() methods will both reject if the responses resulting from any of the requests have a status code that isn't in the 2XX range

const appShellFilesToCache = [
  ...
  'https://workboxjs.org/offline-ga.min.svg'
]

sw-handmade.js

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

  event.waitUntil(
    caches.open('appshell').then((cache) => {
      return cache.addAll(appShellFilesToCache)
    })
  )

})

Solution for no-cors

fetch(event.request).then( response => {

  if (response.ok) {
    let copy = response.clone();
    caches.open('runtime').then( cache => {
      cache.put(request, copy);
    });
    return response;
  }

})

Solution for no-cors

fetch(event.request).then( response => {

  if (response.ok || response.status === 0) {
    let copy = response.clone();
    caches.open('runtime').then( cache => {
      cache.put(request, copy);
    });
    return response;
  }

})

Possible issues

  • We do not know what we get as a response, so there is a chance to cache errors 404, 500, etc.

  • Each cached resource takes at least 7 MB in Cache Storage

App Update

Remember about user experience

TIP #5

Appshell-driven website update

v1

v2

v1

v1

v2

Deployed

In browser

v2

Following the updates

  • In the main script via the registration status of the service worker

navigator.serviceWorker.register('sw-handmade.js')
  .then(registration => {
    if (registration.waiting) {
      // Show "App was updated" prompt to reload
    }
  })
  • In the service worker. After the update happened we inform the app via BroadcastChannel API or postMessage

Inconvenient Truth #2

  • The architecture of the Application Shell goes against the idea of the web about "always fresh"

  • We can only slightly improve the user experience

I'm a PWA and I'm always fresh. But what you see is the outdated version.

Click here to reload

Sometimes service worker boots up not immediately

Don't waste this time!

TIP #6

"Cold start" problem

SW Boot

Navigation request

SW Boot

Navigation request

  • If the service worker is unloaded from memory

  • AND the response of this request is not cached

  • AND the service worker includes a fetch event

addEventListener('activate', event => {
  event.waitUntil(async function() {
    // Feature-detect
    if (self.registration.navigationPreload) {
      // Turn it on!
      await self.registration.navigationPreload.enable();
    }
  }());
});

Navigation preload

addEventListener('fetch', event => {
  event.respondWith(async function() {
    // Best scenario: take if from the Cache Storage
    const cachedResponse = await caches.match(event.request);
    if (cachedResponse) return cachedResponse;

    // OK scenario: use navigation preload response
    const response = await event.preloadResponse;
    if (response) return response;

    // Worst scenario: fetching from the network :(
    return fetch(event.request);
  }());
});

Using the preload result

Not only caching and push notifications

Use the full potential of the service worker

TIP #7

WebP support (w/ WASM)

service-worker.js / fetch event

event.respondWith(async function() {

  const response = await fetch(event.request);
  const buffer = await response.arrayBuffer();

  const WebPDecoder = await fetchWebPDecoder();
  const decoder = new WebPDecoder(buffer);
  const blob = await decoder.decodeToBMP();

  return new Response(blob, { headers: { "content-type": "image/bmp",
    "status": 200 } });

}());

Load balancer

  • Intercept the requests to the resources and select the proper content provider

  • To choose a server with the least load, to test new features, to do A/B testing

Proper tools

Trust AND Check

TIP #8

Frameworks

  • sw-precache / sw-toolbox

  • Workbox

  • offline-plugin for Webpack

  • PWABuilder.com

  • create-react-app

  • preact-cli

  • polymer-cli

  • vue-cli

  • angular-cli

Builders

  • Lighthouse

  • Webhint

Audit / Linting

App shell

Runtime caching

Offline GA

Replay failed requests

Broadcast updates

Build integrations

Possibility to extend your own service worker instead of using generated one

npm install -g hint

hint https://airhorner.com
npm install -g lighthouse

lighthouse https://airhorner.com

Inconvenient Truth #3

  • Even well-known, well-supported open source libraries may contain bugs

  • Even they do not always fast enough in following the specification updates

  1. Loaded ./index.html
  2. Were redirected by 301 to ./ with Content-Type: text/plain
  3. Fetched and cached the content of ./ (which is text/html), but the resulting Content-Type was taken from the request from p.2
  • Update Workbox to 3.6.3

  • Invalidate the existing cache by explicit naming

workbox.core.setCacheNameDetails({precache: 'new-name'});

What

Why

Recent case

In case of emergency

Implement a Kill Switch

TIP #9

  • Unregister service worker?

  • Deploy fixed or no-op service worker

  • Make sure that browser will not serve service worker file(s) from HTTP cache

Rescue plan

No-op

self.addEventListener('install', () => {
  self.skipWaiting();
});
self.addEventListener('activate', () => {
  self.clients.matchAll({type: 'window'}).then(tabs => {
    tabs.forEach(tab => {
      tab.navigate(tab.url);
    });
  });
});

UX breaking

Update service worker

Cache-Control: no-cache

Assets from importScripts()

Main service worker

Spec was updated

Byte-difference check - add versioning via file content

navigator.serviceWorker.register(`/sw.js?v=${VERSION}`);

Byte check doesn't work  - add versioning via filename of imported SW or main SW

updateViaCache

index.html

navigator.serviceWorker.register('/sw.js', {
  updateViaCache: 'none'
})

Values: "imports", "all", or "none"

importScripts() optimizations

  • Well-known scripts are retrieved and boot up even before  importScripts()

  • They are stored as V8 bytecode

Issue

Calling importScripts () at an arbitrary place in the service worker

Solution

Now — only before reaching installed state. The spec was updated.

Upcoming features

Get prepared

TIP #10

Quiz time!

  • No. Spec clearly mentions the same origin

  • Yes. There is an experimental Foreign Fetch

  • Yes. Maybe some other options...

Can service worker be registered without visiting the website? (from another origin)

Payment handler

  • A helper for Payment Request API specifically for the web payment apps

  • Registers some payment instruments (card payments, crypto-currency payments, bank transfers, etc)

  • On payment request user agent computes a list of candidate payment handlers, comparing the payment methods accepted by the merchant with those supported by registered payment handlers

const swReg = await navigator.serviceWorker.register("/sw.js");
await swReg.paymentManager.instruments.set(
  "My Pay's Method ID",
  {
    name: "My Pay's Method",
    method: "https://my.pay/my-method",
  }
);
self.addEventListener("paymentrequest", event => {
  // Open the window with the payment method UI 
  event.openWindow('https://my.pay/checkout')
});

main.js / payment app

sw.js / payment app

Just-In-Time registration

  • The method (via url) is set as supportedMethods in the PaymentRequest of the merchant's website and at the time of payment this method is selected

  • The HEAD request to this url returns the 200 + Link: <address of the Payment Method Manifest>, where the link to Web App Manifest specified in the default_applications

  • In the method's Web App Manifest, there is a serviceworker section with src and scope

  • Service worker from this address will be registered!

  • Its paymentrequest event will be called

Only then...

Background fetch

  • Pause/resume download/upload automatically

  • Aware of fetch status/progress

  • No need to keep the app open

  • UI for the user to pause/cancel and track progress

const registration = await navigator.serviceWorker.ready;
await registration.backgroundFetch.fetch(
  'my-series',
  ['s01e01.mpg', 's01e02.mpg'],
  {
    title: 'Downloading My Series',
    downloadTotal: 600 * 1024 * 1024
   }
);

main.js

addEventListener('backgroundfetchsuccess', event => {
  event.waitUntil(
    (async function() {
      try {
        // Put the responses to Cache Storage
        ...
        await event.updateUI({ title: `Downloaded!` });
      } catch (err) {
        await event.updateUI({ title: `Fail: ${err}` });
      }
    })()
  );
});

src/service-worker.js

More details

Native File System API

Badging API

Contact Picker API

More than 100 new APIs

  • 2000+ developers

  • Major browsers/frameworks/libs devs

TIP #11

Thank you!

@webmaxru

Maxim Salnikov

Questions?

@webmaxru

Maxim Salnikov

Service Worker: taking the best from the past experience for the bright future of PWAs

By Maxim Salnikov

Service Worker: taking the best from the past experience for the bright future of PWAs

There is no doubt that 2018 is the year when Progressive Web Apps will get the really broad adoption and recognition by all the involved parties: browser vendors (finally, all the major ones), developers, users. And the speed and smoothness of this process heavily depend on how correctly we, developers, use the power of new APIs. The main one in PWA concept is Service Worker API, which is responsible for all offline magic, network optimizations and push notifications. In my session based on accumulated experience of developing and maintaining PWAs we - go through the list of advanced tips & tricks, - showcase best practices, - learn how to avoid common pitfalls, - have a look at the latest browser support and known limitations, - share lots of useful insights. All on the way to the #YearOfPWA, all for delighting our users by the truly modern web applications.

  • 3,178