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
-
Web Platform & Cloud speaker, writer, trainer
-
Developer communities facilitator
-
Technical conferences organizer
Developer Audience Lead at Microsoft
Service worker
Networking
Caching
-
Installability
-
Capabilities
PWA chapter of Web Almanac 2021
Power App portals as PWAs
Microsoft 365 web apps as PWAs
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:
-
Populate __WB_MANIFEST
-
Bundle
-
Register in the application
# Using Workbox as a Node module
$ npm install workbox-build
}
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);
Ways to use Workbox
Flexibility
Automating
Modules and methods
Recipes
Service worker generation using CLI
Custom plugins
Custom strategies
-
Full-fledged application platform
-
Offline-readiness APIs are in production
-
Awesome tools are available
-
User experience & security is the key
And this is just the beginning!
Web platform today
-
Demo
-
Source code
-
Extra features
Check this PWA example...
...and host it for free in the cloud
Thank you!
@webmaxru
Maxim Salnikov
Questions?
@webmaxru
Maxim Salnikov
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
}
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 */)
});
}
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.
- 4,059