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.
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
- Loaded ./index.html
- Were redirected by 301 to ./ with Content-Type: text/plain
- 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,182