Angular
Service Workers

Pascal Precht @PascalPrecht
@PascalPrecht
GDE for the Angular Team
Angular Master Class by @thoughtram

Service Worker?

Progressive Web Apps

  • Progressive
  • Responsive
  • Connectivity Independent
  • App-like
  • Fresh
  • Safe
  • Discoverable
  • Re-engageable
  • Installable
  • Linkable

Progressive Web Apps

  • Progressive
  • Responsive
  • Connectivity Independent
  • App-like
  • Fresh
  • Safe HTTPS required!
  • Discoverable
  • Re-engageable
  • Installable
  • Linkable

Service Worker

A script that runs in the background, separate from a web page, opening the door to features that don't need user interaction.

Service Worker Facts

  • JavaScript Worker - Can't access DOM
  • Network Proxy - Controls how network requests are handled
  • Terminated when not in use
  • Safe by default - HTTPS required

Features / Use Cases

  • Reliability - Provide static, dynamic and external resources from cache
  • Performance - Only reach out to network if necessary
  • Push Notifications - Sending push messages to users for re-engagement
  • ...

Service Worker APIs

Worker registration


            if ('serviceWorker' in navigator) {
              navigator.serviceWorker.register('/sw.js').then(() => {
                console.log('Installation successful!');
              }, (err) => {
                console.log('Service Worker registration failed: ', err);
              });
            }
            

info_outline register() can be called every time a page loads without concern.

Worker installation


            self.addEventListener('install', event => {
              event.waitUntil(
                caches.open('aio-static-cache-v1') // different per version
                      .then(cache => {
                        return cache.addAll([
                          '/',
                          '/styles/main.css',
                          '/scripts/main.js'
                        ]);
                      });
              );
            });
            

install event

  • Get things ready before other events are handled
  • event.waitUntil() used to defer (possible) termination
  • Mustn't disrupt previous Service Worker versions

thumb_up Ideal for: Static resources (CSS, JS, images, ...)

Responding from cache


            self.addEventListener('fetch', event => {
              event.respondWith(
                caches.match(event.request)
                      .then(response => {
                        return response ? response : fetch(event.request);
                      })
              );
            });
            

            self.addEventListener('fetch', event => {
              event.respondWith(caches.match(event.request).then(response => {
                if (response) return response;
                const request = event.request.clone();
                return fetch(request).then(response => {
                  if (!response || response.status !== 200) { 
                    return response;
                  }
                  const responseToCache = response.clone();
                  caches.open('aio-dynamic-cache').then(cache => {
                    cache.put(event.request, responseToCache);
                  });
                  return response;
                });
              }));
            });
            

fetch event

  • Executed on every request made by browser
  • Good place to cleanup cache (e.g. Flickr images)
  • Request and Response streams can only be consumed once - needs clone

thumb_up Ideal for: Frequently updating resources

Updating Workers


            self.addEventListener('activate', event => {
              const cacheWhitelist = ['aio-static-cache-v2'];

              event.waitUntil(
                caches.keys().then(cacheNames => Promise.all(
                  cacheNames.map(cacheName => {
                    if (cacheWhitelist.indexOf(cacheName === -1)) {
                      return caches.delete(cacheName);
                    }
                  })
                ));
              );
            });
            

activate event

  • Called when previous Service Worker deactivated
  • Long activation could potentially block page loads

thumb_up Ideal for: Cleanup and migration

Congrats, you know Service Workers.

Angular Service Worker

Angular Service Worker

  • Basically a ready-to-use implementation
  • Consumes manifest for configuration
  • Plugin system for extensibility

Takes care of bulk work

  • Installing/updating Service Workers
  • Managing caches (yea, we don't have to do that)
  • Handles dynamic routes and static, dynamic and external resources
  • ...

Installation

Angular Service Worker implementation comes in an additional module


            $ npm install @angular/service-worker
            

info_outline Angular CLI takes care of adding the worker to our app*
* When using Push APIs need to do a bit more work

Enabling SW Support


            // .angular-cli.json
            {
              ...
              "apps": [
                {
                  ...
                  "serviceWorker": "true"
                }
              ]
            }
            

            $ ng set apps.0.serviceWorker=true
            

Angular CLI will now add a Service Worker registration script to our production build


            <!doctype html>
            <html>
              ...
              <body>
                <aio-shell></aio-shell>
                <script src="polyfills.{HASH}.bundle.js"></script>
                <script src="sw-register.{HASH}.bundle.js"></script>
                <script src="main.{HASH}.bundle.js"></script>
              </body>
            </html>
            

Behind the scenes

  • Adds script for Service Worker registration to index.html
  • Copies Service Worker bundle to dist/
  • Generates a ngsw-manifest.json

info_outline Service Worker support only works in production builds

ngsw-manifest

ngsw-manifest

Manifest that configures plugins for the Angular Service Worker. This includes:

  • Static and external content resources
  • Dynamic resources
  • Routing redirection
  • ...

Static Content Cache


            // ngsw-manifest.json
            {
              "static": {
                "index.html": "9cd2a887...",
                "main.bundle.js": "1cab6cf...",
                "main.css": "c0ee6a6..."
              },
              ...
            }
            

info_outline Hashes are used by Service Worker to identify new versions of the requested resources

External Content Cache


            // ngsw-manifest.json
            {
              ...
              "external": {
                "urls": [
                  { "url": "https://fonts.googleapis.com/css?family=..." },
                  { "url": "https://fonts.gstatic.com/s/materialicons/..." }
                ]
              }
            }
            

thumb_up Useful for fonts and other files we don't serve ourselves

Route Redirection

  • Service Worker only serves content from cache listed in manifest
  • Requests to e.g. /some/sub-route will still fail

info_outline Route redirection takes care of redirecting configured routes
to a specific index route


            // ngsw-manifest.json
            {
              ...
              "routing": {
                "index": "/index.html",
                "routes": {
                  "/": {
                    "prefix": "false" // match exactly
                  }
                }
              }
            }
            

            // ngsw-manifest.json
            {
              ...
              "routing": {
                "index": "/index.html",
                "routes": {
                  "/blog": {
                    "prefix": "true" // match '/blog*'
                  }
                }
              }
            }
            

            // ngsw-manifest.json
            {
              ...
              "routing": {
                "index": "/index.html",
                "routes": {
                  "/(?!e?plnkr)[^/.]*$": {
                    "match": "regex"
                  }
                }
              }
            }
            

Dynamic Content Cache

By default, files fetched at runtime aren't cached. Angular helps with two optimization options:

  • performance - Always serve from cache first, then try to cache updates
  • freshness - Always try to fetch first, fallback to cache

            {
              ...
              "dynamic": {
                "group": [
                  {
                    "name": "guides",
                    "urls": {
                      "/generated/docs/guide": { "match": "prefix" }
                    },
                    "cache": {
                      "optimizeFor": "performance", // or 'freshness'
                      "maxAgeMs": "360000000",
                      "maxEntries": 1,
                      "strategy": "lru" // or 'fifo', 'lfu'
                    }
                  }
                ]
              }
            }
            

Demo

Doing more...

  • Check for updates at runtime - Prompt users to restart page
  • Generate SW routes - Manifest updates routing config automatically
  • Delay Service Worker registration - Don't compete for resources with critical path of page boot

Other features

  • Push Notifications - Re-engage with your users by sending notifications
  • Background Sync - Defer actions until user has stable connectivity (no special support in ngsw)
  • ...

Credits

  • Jake Archibald
  • Alex Rickabaugh
  • Maxim Salnikov
  • Ari Lerner

And... Everyone who worked on the latest angular.io release!

Thank you.

Pascal Precht @PascalPrecht