Maxim Salnikov

@webmaxru

Automating a service worker with

Workbox 6

How to improve a web app performance and UX

while having a great DX

Maxim Salnikov

  • PWA Oslo / PWA London meetups organizer

  • ngVikings / Mobile Era conferences organizer

  • Google Dev Expert in Web Tech / Capabilities & Installability

Developer Audience Lead at Microsoft

Agenda

  • What is a PWA and why

  • Service worker it's not simple

  • Common tasks with the Workbox

  • Recipes

  • Extensibility

  • Useful resources

How is it going with PWA?

Privacy

Functionality

Was web forked?

  • Web standards

  • Progressive enhancement

}

Service worker

Networking

Caching

  • Installability

  • Capabilities

Controlled by service worker

Pages

0.9%

mobile

1%

desktops

Controlled by service worker

Page loads

18.2%

User friendly

  • App itself

  • Online runtime data

  • Offline runtime data

  • Connection failures

  • Updates

  • Platform features

  • Always available

  • Thoughtfully collected

  • Safely preserved

  • Do not break the flow

  • Both explicit and implicit

  • For the win!

While keeping its web nature!

A joke from 2019

In theory

self.addEventListener('install', event => {
    // Putting resources into the Cache Storage
})

self.addEventListener('activate', event => {
    // Managing versions
})

self.addEventListener('fetch', event => {
    // Exctracting from the cache and serving
})

handmade-service-worker.js

In the guide

const PRECACHE = 'precache-v1';
const RUNTIME = 'runtime';

const PRECACHE_URLS = [
  'index.html',
  './',
  'styles.css',
  '../../styles/main.css',
  'demo.js'
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(PRECACHE)
      .then(cache => cache.addAll(PRECACHE_URLS))
      .then(self.skipWaiting())
  );
});

self.addEventListener('activate', event => {
  const currentCaches = [PRECACHE, RUNTIME];
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
    }).then(cachesToDelete => {
      return Promise.all(cachesToDelete.map(cacheToDelete => {
        return caches.delete(cacheToDelete);
      }));
    }).then(() => self.clients.claim())
  );
});

self.addEventListener('fetch', event => {
  if (event.request.url.startsWith(self.location.origin)) {
    if (event.request.url.indexOf('api/') != -1) {
      event.respondWith(
        caches.match(event.request.clone()).then((response) => {
          return response || fetch(event.request.clone()).then((r2) => {
            return caches.open(RUNTIME).then((cache) => {
              cache.put(event.request.url, r2.clone());
              return  r2.clone();
            });
          });
        })
      );
    } else {
      event.respondWith(
        caches.match(event.request).then(cachedResponse => {
          if (cachedResponse) {
            return cachedResponse;
          }
          return caches.open(RUNTIME).then(cache => {
            return fetch(event.request).then(response => {
              return cache.put(event.request, response.clone()).then(() => {
                return response;
              });
            });
          });
        })
      );
    }
  }
});

handmade-service-worker.js

− Build automation

− Configurability

− Extensibility

− Smart caching

− All possible fallbacks

− Communication with the app

− Debug information 

− ...

In production

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!!!

  • Balanced abstraction level

  • Declarativeness where appropriate

  • Modularity and extensibility

  • Rich functionality out of the box

  • Powerful tooling 

~30% of the service workers — are...

Open source, active maintenance and support 

Implementing offline-readiness

import { precacheAndRoute } from "workbox-precaching";

// Cache and serve resources from __WB_MANIFEST array
precacheAndRoute(self.__WB_MANIFEST);

src/service-worker.js

Remaining steps:

  1. Populate __WB_MANIFEST

  2. Bundle

  3. Register in the application

 

# Using Workbox as a Node module
$ npm install workbox-build --save-dev

}

On every app build

Build script

const { injectManifest } = require("workbox-build");

let workboxConfig = {
  swSrc: "src/service-worker.js",
  swDest: "dist/sw.js",
  globPatterns: ["index.html", "*.css", "*.js"]
};

injectManifest(workboxConfig).then(() => {
  console.log(`Generated ${workboxConfig.swDest}`);
});

sw-build.js

[Almost] ready service worker

import { precacheAndRoute } from "workbox-precaching";

precacheAndRoute([
  { revision: "866bcc582589b8920dbc", url: "index.html" },
  { revision: "c2761edff7776e1e48a3", url: "styles.css" },
  { revision: "3469613435532733abd9", url: "main.js" }
]);

dist/sw.js

import { precacheAndRoute } from "workbox-precaching";

precacheAndRoute(self.__WB_MANIFEST);

src/service-worker.js

Bundling and minifying

import resolve from 'rollup-plugin-node-resolve'
import replace from 'rollup-plugin-replace'
import { terser } from 'rollup-plugin-terser'

export default {
  input: 'dist/sw.js',
  output: {
    file: 'dist/sw.js',
    format: 'iife'
  },
  plugins: [ /* On the next slide */ ]
}

rollup.config.js

Configuring plugins

plugins: [
  resolve(),
  replace({
    'process.env.NODE_ENV': JSON.stringify('production')
  }),
  terser()
]

rollup.config.js

App build integration

"build-pwa":
  "npm run build-app &&
   node build-sw.js &&
   npx rollup -c"

package.json / scripts

Registering in the app

import { Workbox, messageSW } from 'workbox-window';

if ('serviceWorker' in navigator) {
      const wb = new Workbox('/sw.js');      
      wb.register();
    
      // Interactive update flow with messageSW
      // code sample is at https://aka.ms/workbox6
}

src/main.js

New version is available. Click to reload.

  • Works offline (application shell)

  • Managing versions

  • Detailed debug information 

Runtime caching

import { registerRoute } from "workbox-routing";
import {
  CacheFirst,
  NetworkFirst,
  StaleWhileRevalidate,
} from "workbox-strategies";

src/service-worker.js

// Avatars can always be taken from the cache
registerRoute(
  new RegExp("https://www.gravatar.com/avatar/.*"),
  new CacheFirst()
);

API responses caching

// Keeping article list always fresh
registerRoute(
  ({url}) => url.pathname.startsWith('/api/articles/'),
  new NetworkFirst()
);

// Retrieving article from the cache and checking for updates
import { BroadcastUpdatePlugin } from 'workbox-broadcast-update';

registerRoute(
  ({url}) => url.pathname.startsWith('/api/article/'),
  new StaleWhileRevalidate({
    plugins: [
      new BroadcastUpdatePlugin()
    ],
  })
);

src/service-worker.js

Strategies

  • CacheFirst

  • CacheOnly

  • NetworkFirst

  • NetworkOnly

  • StaleWhileRevalidate

  • ...custom strategy?

Plugins

  • Expiration

  • CacheableResponse

  • BroadcastUpdate

  • BackgroundSync

  • ...custom plugin?

Recipes

import {
  googleFontsCache,
  imageCache
} from "workbox-recipes";

// Caching Google Fonts
googleFontsCache({ cachePrefix: "wb6-gfonts" });

// Caching images
imageCache({ maxEntries: 10 });

src/service-worker.js

...and one-liners for:

// Pages and images fallback
offlineFallback();

// Pages caching
pageCache();

// Static assets caching
staticResourceCache();

// Cache warming up
warmStrategyCache(urls, strategy);

Implementing a custom strategy

import {Strategy} from 'workbox-strategies';

class CacheNetworkRace extends Strategy {
  async _handle(request, handler /* StrategyHandler ins. */) {
    // Requesting, processing, returning results
  }
}
  • If you need to change the request logic

  • Can be used in registerRoute ()

  • Standard plugins can be used 

StrategyHandler methods

_handle(request, handler) {
  const fetchDone = handler.fetchAndCachePut(request);
  const matchDone = handler.cacheMatch(request);

  return new Promise((resolve, reject) => {
    fetchDone.then(resolve);
    matchDone.then((response) => response && resolve(response));

    Promise.allSettled([fetchDone, matchDone]).then(/* reject */)
  });
}

Ways to use Workbox

Flexibility

Automating

Modules and methods

Recipes

Service worker generation using CLI

Custom plugins

Custom strategies

  • Demo

  • Source code

  • Deployed to Azure Static Web Apps

  • 2000+ developers

  • Major browsers/frameworks/libs reps

Thank you!

@webmaxru

Maxim Salnikov

Automating a service worker with Workbox 6

By Maxim Salnikov

Automating a service worker with Workbox 6

"I deployed a service worker - now, I need to buy a new domain" - a well-known joke about the difficulty of implementing your own caching logic. With the arrival of the sixth version of the Workbox library, the trade-off between flexibility and ease of automation of network tasks for PWA is no longer needed. In this talk, I will tell you how to get started with Workbox 6, implement typical functionality for an offline web application, and go further by adding your own caching logic.

  • 86
Loading comments...

More from Maxim Salnikov