before your PWA go into the wild

Maxim Salnikov

@webmaxru

Tame your Service Worker

How to build fast, offline-ready, user-friendly web apps

And enjoy doing this

Maxim Salnikov

@webmaxru

  • Google Developer Expert in Angular

  • Mobile Oslo / Angular Oslo / PWA Oslo meetups organizer

  • Mobile Era conference organizer

Products from the future

UI Engineer at ForgeRock

Service worker

What kind of animal is it?

Progressive Web App

... web apps that use the latest web technologies.

... attempts to combine features offered by most modern browsers with the benefits of mobile experience

10 characteristics

  • Progressive

  • Discoverable

  • Linkable

  • App-like

  • Responsive

  • Connectivity-independent

  • Re-engageable

  • Installable

  • Fresh

  • Safe

Service Worker API

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

Service worker

  • Make some features of the web app function offline

  • Improve online performance by reducing network requests

Not only networking

  • Receiving push events and displaying notifications

  • Clients (tabs) messaging

  • Job scheduling

  • Responding to resource requests from other origins

Logically

Physically

JS

-file

App

Service worker

Lifecycle

'install'

Parsed

Installing

Activating

Redundant

'activate'

Waiting

Active

In Development

Behind the flag

Is service worker ready?

Progressive Web Apps

favoring or advocating progress, change, improvement, or reform

happening or developing gradually or in stages

Progressive enhancement

Go for the feature detection

#1

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) {

  // Consider using action buttons

}

?

Proper time to register

The later the better

#2

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(...);
    });
}
platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .then(() => {

    // Service worker registration

  });

main.ts

// Registered
navigator.serviceWorker.register('/news/sw.js', {scope: '/news/2017/'});

Scope

// Registered if 'Service-Worker-Allowed' header is set to ‘/’
// NOT registered
navigator.serviceWorker.register('/news/sw.js', {scope: '/catalog/'});
// NOT registered
navigator.serviceWorker.register('/news/sw.js', {scope: '/'});

In case of emergency

Implement a Kill Switch

#3

No animals were harmed

  • Deploy fixed or no-op service worker

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

  • ... or unregister service worker

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 & bypass HTTP cache

Cache-Control: max-age=0

Server side

Service worker

Spec will be updated

  • Byte-different - add versioning

  • Byte check doesn't work for scripts imported with importScripts()

Unregister

navigator.serviceWorker.getRegistrations()
    .then((registrations) => {
        for(let registration of registrations) {
            registration.unregister()
        }
    })

index.html

Pre-caching

Trust your assets and be intolerant to the outdated ones

#4

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

sw-handmade.js

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

  event.waitUntil(
    caches.open(cacheName).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

  • Quota Management API

  • Storage Quota Estimate API

 

Caching from other origins

Get ready for opaque

#5

3 options

  • Add CORS headers on remote side

  • Handle opaque responses

  • Use experimental foreign fetch

Opaque responses limitations

  • Valid for limited set of elements: <script>, <link rel="stylesheet">, <img>, <video>, <audio>, <object>, <embed>, <iframe>

  • 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(cacheName).then((cache) => {
      return cache.addAll(appShellFilesToCache)
    })
  )

})

Solution for no-cors

const noCorsRequest =
    new Request('https://workboxjs.org/offline-ga.svg', {
        mode: 'no-cors'
    });

fetch(noCorsRequest)
    .then(response => cache.put(noCorsRequest, response));

Redirects

To follow or not to follow?

#6

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

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

})
const appShellFilesToCache = [
    ...
    './assets/redirect/redirectfrom.html'
]

sw-handmade.js

app.get('/assets/redirect/redirectfrom.html', (req, res) => {
    res.redirect(301, '/assets/redirect/redirectto.html')
})

server/index.js

If one of the following conditions is true, then return a network error:

  • ...

  • request’s redirect mode is not "follow" and response’s url list has more than one item.

Redirect mode of navigation request is “manual”

 

  • To avoid the risk of open redirectors introduce a new security restriction which disallows service workers to respond to requests with a redirect mode different from "follow".

  • Add .redirected attribute to Response class of Fetch API. Web developers can check it to avoid untrustworthy responses.

  • Do not pre-cache redirected URLs

  • "Clean" the response before responding

Solutions for 3xx

Do not pre-cache 3xx at all

/dashboard

/dashboard

/login

// If "cleanRedirects" and this is a redirected response,
// then get a "clean" copy to add to the cache.

const newResponse = cleanRedirects && response.redirected ?
    await cleanResponseCopy({response}) :
    response.clone();

workbox/.../request-wrapper.js#L420

"Clean" the response

Proper tools

Be there or be square

#7

Tools helps with

  • Implementing complex algorithms

  • Following specifications updates

  • Handling edge cases

  • Adopting best practices

  • Focusing on YOUR task

Frameworks

  • sw-precache / sw-toolbox

  • Workbox

  • offline-plugin for Webpack

  • create-react-app

  • preact-cli

  • polymer-cli

  • vue-cli

  • angular-cli

Generators

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

Testing

Service Worker Mock

const env = {
  // Environment polyfills
  skipWaiting: Function,
  caches: CacheStorage,
  clients: Clients,
  registration: ServiceWorkerRegistration,
  addEventListener: Function,
  Request: constructor Function,
  Response: constructor Function,
  URL: constructor Function
};

Push notifications

Don't convert them to "new pop-ups"

#8

  • Initiate only after explicit user action

  • Keep unsubscribe functionality visible 

Subscription

Notifications

  • Consider alternative ways first - displaying notification is the last resort

  • Not for broadcasting, but for individual approach

MyAirline

Online check in is available

MyAirline

myairline.com

Flight

DY1043

Depart

21.09 13:45

OSL -> LAX

Click here to check in now

myairline.com

Content

  • Do not Repeat Yourself

  • Provide actual information instead of notifying about it

  • Give a call to action

If Web Push doesn't work

  • Subscription is valid 

  • Event listener push exists

  • There is self.registration.showNotification()

Upcoming features

Not so mature yet

#9

Navigation preload

SW Boot

Navigation request

SW Boot

Navigation request

self.addEventListener('activate', e => {
  e.waitUntil(self.registration.navigationPreload.enable());
});
self.addEventListener('fetch', event => {
  event.respondWith(async function() {
    // Respond from the cache if we can
    const cachedResponse = await caches.match(event.request);
    if (cachedResponse) return cachedResponse;

    // Else, use the preloaded response, if it's there
    const response = await event.preloadResponse;
    if (response) return response;

    // Else try the network.
    return fetch(event.request);
  }());
});

Periodic sync

  • Restricted by time interval, battery state and network state

  • Would require user permission

  • Don't require any server configuration

  • Allow the user agent to optimize when they fire

navigator.serviceWorker.ready.then((registration) => {
  registration.periodicSync.register({
    tag: 'get-latest-news',         // default: ''
    minPeriod: 12 * 60 * 60 * 1000, // default: 0
    powerState: 'avoid-draining',   // default: 'auto'
    networkState: 'avoid-cellular'  // default: 'online'
  }).then((periodicSyncReg) => {

    // Successfully registered

  })
});

index.html

self.addEventListener('periodicsync', function(event) {
  if (event.registration.tag == 'get-latest-news') {
    event.waitUntil(fetchAndCacheLatestNews());
  }
  else {
    // Unknown sync, may be old, best to unregister
    event.registration.unregister();
  }
});

sw-handmade.js

Foreign fetch

  • Intercepting cross-origin requests from any client

  • Common resource cache

  • Network-independent version of service

CDN

API

Images

Analytics

Link: </sw-fontservice.js>; rel="serviceworker"; scope="/"
self.addEventListener('install', event => {
  event.registerForeignFetch({
    scopes: ['/fonts'], // or self.registration.scope
    origins: ['*'] // or ['https://mycustomer.com']
  });
});

self.addEventListener('foreignfetch', ...)

myfont.woff

sw-fontservice.js

  • 700+ developers

  • Major browsers/frameworks/libs devs

#10

Tusen takk!

@webmaxru

Maxim Salnikov

Questions?

Made with Slides.com