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