Maxim Salnikov
@webmaxru
Service worker:
Taking the best from the past experience for the bright future of PWAs
What did we learn from
3 years of exploring PWA idea?
And where do we go next?
June 15, 2015
Maxim Salnikov
@webmaxru
-
"PWAngelist"
-
PWA Oslo / PWA London meetups, PWA slack organizer
-
Mobile Era / ngVikings conferences organizer
Products from the future
UI Engineer at ForgeRock
Our plan for today
-
PWA status
-
Service worker's life
-
HTTP(S) games
-
Tooling
-
A word about UX
-
Beyond the networking
-
Get ready for the future
#YearOfPWA
Latest updates
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.
Cross-platform?
Browser
Desktop
Mobile
Flagged
OS
#YearOfPWA
Earlier in 2018
UX advantages?
Smart networking + Offline
Proper app experience
Staying notified
Other cool things
}
Service Worker
API
Web App Manifest
Service worker
The ❤️ of PWA
Logically
"Physically"
JS
-file(s)
Web App
Service worker
Browser / WebView
Event-driven worker
Lifecycle
'install'
Parsed
Installing
Activating
Redundant
'activate'
Waiting
Active
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
TIP #0
Progressive enhancement
Go for the feature detection
TIP #1
Platforms / browsers support
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
TIP #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
In case of emergency
Implement a Kill Switch
TIP #3
-
Deploy fixed or no-op service worker
-
... or unregister 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
Unregister
navigator.serviceWorker.getRegistrations()
.then((registrations) => {
for(let registration of registrations) {
registration.unregister()
}
})
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim());
self.registration.unregister();
});
service-worker.js
Chicken-and-egg
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"
Pre-caching
Trust your assets and be intolerant to the outdated ones
TIP #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
if ('storage' in navigator && 'estimate' in navigator.storage) {
navigator.storage.estimate().then(({usage, quota}) => {
console.log(`Using ${usage} out of ${quota} bytes.`);
});
}
Caching from other origins
Get ready for opaque
TIP #5
2 options
-
Add CORS headers on remote side
-
Handle opaque responses
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?
TIP #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
TIP #7
Tools help with
-
Implementing complex algorithms
-
Adopting best practices
-
Focusing on YOUR task
-
Following specifications updates
-
Handling edge cases
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
-
Sonarwhal
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
Manifest checker & generator
Service worker builder
Manifest icons generator
Generating the projects you need to build native apps for the stores
I have one "real" codebase, the PWA. Then I have 3 "wrapper" projects: Windows UWP app, Java app for Android Studio, Objective C app for XCode. All 3 wrapper projects load up the PWA.
npm install -g sonarwhal
sonarwhal --init
sonarwhal https://airhorner.com
npm install -g lighthouse
lighthouse https://airhorner.com
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
};
App Update
Don't break the web
TIP #8
App version updates
v1
v2
v1
v1
v2
Server
Browser
v2
A newer version of the app is available. Refresh
App update strategies
-
Check on app start
-
Check periodically
-
Check on navigation requests
const updatesChannel = new BroadcastChannel('precache-updates');
updatesChannel.addEventListener('message', event => {
console.log('Cache updated', event.data.payload.updatedUrl);
// Show a prompt "New version is available. Refresh?"
});
workbox.precaching.addPlugins([
new workbox.broadcastUpdate.Plugin('precache-updates')
]);
main.js
service-worker.js
App Installation
Choose the proper moment
TIP #9
-
Show up moment is unpredictable. Heuristics is still under adjustment.
-
Interrupting user experience (incl. covering the content on mobile device)
-
Well hidden way to do it manually
-
Not very discoverable alternative: ambient badging
Default prompt
Ongoing discussion
Show your own UI
main.js
window.addEventListener("beforeinstallprompt", event => {
// Suppress automatic prompting.
event.preventDefault();
// Show custom install button
installButton.classList.remove('hidden');
// Bind onclick event - on the next slide
});
Show native prompt
main.js
installButton.addEventListener("click", e => {
event.prompt(); // event - from beforeinstallprompt
event.userChoice
.then( choiceResult => {
console.log(`Choice is: ${choiceResult.outcome}`);
// Hide custom install button
installButton.classList.add('hidden');
})
});
Track all possible ways
main.js
window.addEventListener("appinstalled", event => {
console.log('The app was installed')
// Hide custom install button
installButton.classList.add('hidden');
});
-
Give the developer ability to show installation prompt at any time?
-
Install the app without visiting the website
-
Push the app directly to the devices
Open questions
Push notifications
Use responsibly
TIP #10
-
Initiate only after explicit user action
-
Keep unsubscribe functionality visible
Subscription
Allow users to unsubscribe.
Otherwise they'll block!
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
Beyond the caching
Use the full potential of the service worker
TIP #11
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
Client-side downloads
service-worker.js
self.addEventListener('fetch', function(event) {
if(event.request.url.indexOf("download-file") !== -1) {
event.respondWith(event.request.formData().then( formdata => {
var response = new Response(formdata.get("filebody"));
response.headers.append('Content-Disposition',
'attachment; filename="' + formdata.get("filename") + '"');
return response;
}));
}
});
WebP support (with WASM)
service-worker.js
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 } });
}());
Upcoming features
Get prepared
TIP #12
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
Background fetch
Fetches (requests & responses) are alive after user closes all windows & worker to the origin
Browser/OS shows UI to indicate the progress of the fetch, and allow the user to pause/abort
Dealing with poor connectivity by pausing/resuming the download/upload
App has an access to the fetched resources and to the status/progress of the fetch
const registration = await navigator.serviceWorker.ready;
const bgFetchJob =
await registration.backgroundFetch.fetch(id, requests, options);
addEventListener('backgroundfetched', event => {
event.waitUntil(async function() {
const fetches = await event.fetches.values();
// Put all fetch responses to the cache
...
}());
});
main.js
service-worker.js
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.paymentInstruments.set(
"c8126178-3bba-4d09-8f00-0771bcfd3b11",
{
name: "My Bob Pay Account: john@example.com",
method: "https://bobpay.com",
icons: [{ ... }]
}
);
self.addEventListener("paymentrequest", event => {
// Do the payment flow
// Open window if needed
});
main.js
service-worker.js
-
1800+ developers
-
Major browsers/frameworks/libs devs
TIP #13
Thank you!
@webmaxru
Maxim Salnikov
Questions?
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.
- 6,350