Maxim Salnikov
@webmaxru
Diving deep, deep offline without drowning –
the web can do it!
What is an offline-ready web application
And how to build it today
Maxim Salnikov
-
PWA Slack organizer
-
PWA Oslo / PWA London meetups organizer
-
PWA speaker and trainer
-
Google Dev Expert in Web Technologies
Azure Developer Technical Lead at Microsoft
-
The World Wide Web is the New Software Platform
-
The Web Browser is the New Operating System
-
JavaScript is the de facto Programming Language of the Web
January, 2008
Web as an app platform is amazing
Browsers
-
Almost on every device with UI
-
Evergreen
-
APIs to access device hardware
Web as an app platform is amazing
JavaScript
Browsers
-
Versatile language
-
Powerful tooling
-
Evolving in a smart way
Web as an app platform is amazing
JavaScript
JS Engines
Browsers
-
Focus on the performance
-
Embedding possibilities
Web as an app platform is amazing
JavaScript
JS Engines
UI Layer
Browsers
-
Convenient tools to build responsive UIs
-
Focus on accessibility
-
Variety of high-quality components and libraries
Web as an app platform is amazing
Community
JavaScript
JS Engines
UI Layer
Browsers
69.7%
Web as an app platform is amazing
Community
JavaScript
JS Engines
UI Layer
Browsers
Issues?
Historically depends on the "connection status"
Solutions
Caching
Installing
-
HTTP Cache?
-
AppCache
-
Save page as... (complete)
-
Chrome Apps
-
Electron
-
NativeScript, React Native
What is PWA at all?
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
Offline-
ready
<
>
=
=
+
Application shell
Web App Manifest
Fast, responsive, mobile-first
Served via HTTPS
Offlinization Task #1
Application UI (App Shell)
We want our offline-ready webapp to
-
Be available itself regardless of the connection status and be always updated
-
Thoughtfully collect and store data received from the network during runtime (API, CDN, etc) to reuse it later
-
Preserve the actions and data generated by user while offline to automatically sync it after connection restored
-
Keep full control on fetching larger resources, provide progress info, have an access to downloaded data
Exactly like native apps do! (or better)
Let's build an App shell
My App
-
Define the minimal set of resources we need, and keep versioning in mind
-
On the first app load, explicitly put these resources into the Cache Storage
-
On the next app loads, serve resources from this cache before going to the network
-
At the same time, check if the never version is available. If yes, update the cache
Service Worker 101
Logically
Physically
-file(s)
App
Service-worker
Browser/OS
Event-driven worker
Cache
fetch
push
sync
Managing cache
self.addEventListener('install', event => {
// Use Cache API to cache html/js/css
})
self.addEventListener('activate', event => {
// Clean the cache from the obsolete app shell versions
})
handmade-service-worker.js
In the real world
-
Can't add opaque responses directly
-
Redirected requests should be managed
-
Always creating a new version of cache and deleting the old one is not optimal
-
Control over cache size is required
-
Cache invalidation for runtime caching is complex
-
...
Intercepting requests
self.addEventListener('fetch', event => {
if (event.request.url.indexOf('/api') != -1) {
event.respondWith(
// Network-First Strategy (for API?)
)
} else {
event.respondWith(
// Cache-First Strategy (for app shell?)
)
}
})
handmade-service-worker.js
In the real world
-
All kinds of fallbacks needed for the strategies
-
There are more complex strategies like Stale-While-Revalidate
-
Good to have some form of the routing
-
Good to have the possibility to provide some extra settings for different resource groups
-
...
-
Application shell
-
Runtime caching
-
Replaying failed network requests
-
Offline Google Analytics
-
Broadcasting updates
Have our own service worker!
Working modes
-
Workbox CLI
-
Webpack plugin
-
Node module
# Installing the Workbox Node module
$ npm install workbox-build --save-dev
Build script
// We will use injectManifest mode
const {injectManifest} = require('workbox-build')
// Sample configuration with the basic options
var workboxConfig = {...}
// Calling the method and output the result
injectManifest(workboxConfig).then(({count, size}) => {
console.log(`Generated ${workboxConfig.swDest},
which will precache ${count} files, ${size} bytes.`)
})
workbox-build-inject.js
Build script configuration
// Sample configuration with the basic options
var workboxConfig = {
globDirectory: 'dist/',
globPatterns: [
'**/*.{txt,png,ico,html,js,json,css}'
],
swSrc: 'src/workbox-service-worker.js',
swDest: 'dist/sw.js'
}
workbox-build-inject.js
Application Shell
// Importing Workbox itself from Google CDN
importScripts('https://googleapis.com/.../workbox-sw.js');
// Precaching and setting up the routing
workbox.precaching.precacheAndRoute([])
src/workbox-service-worker.js
Caching, serving, managing versions
Precaching manifest
[
{
"url": "index.html",
"revision": "34c45cdf166d266929f6b532a8e3869e"
},
{
"url": "favicon.ico",
"revision": "b9aa7c338693424aae99599bec875b5f"
},
...
]
Build flow integration
{
"scripts": {
"build-pwa": "npm run build-app &&
node workbox-build-inject.js"
}
}
package.json
Offlinization Task #1.1
Better UX for update flow
App version updates
v1
v2
v1
v1
v2
Deployed
Displayed
v2
A new version of the app is available. Click to refresh.
const updateChannel = new BroadcastChannel('app-shell');
updateChannel.addEventListener('message', event => {
// Inform about the new version & prompt to reload
});
Option #1: BroadcastChannel
main.js
workbox.precaching.addPlugins([
new workbox.broadcastUpdate.Plugin('app-shell')
]);
src/workbox-service-worker.js
3
Option #2: Service worker lifecycle
Option #3: workbox-window from v4
Offlinization Task #2
Application Data
Intercepting requests
self.addEventListener('fetch', event => {
if (event.request.url.indexOf('/api/breakingnews') != -1) {
event.respondWith(
// Network-First Strategy
)
} else if (event.request.url.indexOf('/api/archive') != -1 {
event.respondWith(
// Cache-First Strategy
)
}
})
handmade-service-worker.js
Routes and strategies
workbox.routing.registerRoute(
new RegExp('/api/breakingnews'),
new workbox.strategies.NetworkFirst()
);
src/workbox-service-worker.js
workbox.routing.registerRoute(
new RegExp('/api/archive'),
new workbox.strategies.CacheFirst({
plugins: [...]
})
);
Strategies
-
CacheFirst
-
CacheOnly
-
NetworkFirst
-
NetworkOnly
-
StaleWhileRevalidate
Plugins
-
Expiration
-
CacheableResponse
-
BroadcastUpdate
-
BackgroundSync
-
...your own plugin?
Offlinization Task #3
Save and sync offline actions
Background sync
-
Deferring the actions until the user has stable connectivity
-
The sync will be performed even when the application is not running
navigator.serviceWorker.ready.then( swRegistration => {
return swRegistration.sync.register('myFirstSync');
});
main.js
self.addEventListener('sync', event => {
if (event.tag == 'myFirstSync') {
event.waitUntil(
// Do useful things...
);
}
});
handmade-service-worker.js
const postTweetPlugin =
new workbox.backgroundSync.Plugin('tweetsQueue', {
maxRetentionTime: 24 * 60 // Max retry period
})
src/workbox-service-worker.js
workbox.routing.registerRoute(
/(http[s]?:\/\/)?([^\/\s]+\/)post-tweet/,
new workbox.strategies.NetworkOnly({
plugins: [postTweetPlugin]
}),
'POST'
)
Offlinization Task #4
Full control over downloads
Background fetch
-
Dealing with poor/no connectivity by pausing/resuming the download/upload
-
App has an access to the fetched resources and to the status/progress of the fetch
-
Fetches (requests & responses) are alive after user closed all application tabs
-
Browser/OS shows UI to indicate the progress of the fetch, and allow the user to pause/abort
const registration = await navigator.serviceWorker.ready;
await registration.backgroundFetch.fetch(
'my-series',
['s01e01.mpg', 's01e02.mpg'],
{
title: 'Downloading My Series',
downloadTotal: 1000000000
}
);
main.js
const bgFetches =
await registration.backgroundFetch.getIds();
console.log(bgFetches);
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
Summary
-
The web is a full-fledged application platform
-
Foundations of offline-ready mechanisms are in production in all modern browsers
-
Awesome tools are also available
-
Keeping focus on user needs and experience is the key
"Progressive Web Apps State of the Union" by Dominick Ng at BlinkOn 10
-
2000 developers
-
Major browsers/frameworks/libs reps
Thank you!
Maxim Salnikov
@webmaxru
Questions?
Maxim Salnikov
@webmaxru
#WSH?
Diving deep, deep offline without drowning – the web can do it!
By Maxim Salnikov
Diving deep, deep offline without drowning – the web can do it!
There is no need to advocate for progressive web apps anymore. The idea of connection-independent applications has proven its viability and we see more and more large and small projects following that path, making the offline-ready behavior a best practice, good manner of the web. In my session, based on the deep exploration of Service Worker API possibilities (using Cache Storage, Background Fetch, Background Sync) and gathered UX gotchas, we go through the history of the offline web, the importance of treating the network as an enhancement, current challenges (and their solutions) and proper tooling. We architect our offline-ready app applying the best tech and UX practices and adding the features one-by-one: app shell, caching resources and data, sync when online. All in name of our users who demand the new level of the resilient web experience. The section is mix of visionary and technical pieces. During „building“ our offline-ready app, we go through code samples, UX best practices, learn how to avoid pitfalls, find workarounds for non-supported features.
- 4,054