PWAs Five Years On

A presentation at An Event Apart Spring Summit in April 2021 in by Ire Aderinokun

Slide 1

Slide 1

PWAs in 2021 rt Ire Aderinokun @ An Event Apa “Spring Summit” 2021

Slide 2

Slide 2

Slide 3

Slide 3

Slide 4

Slide 4

Slide 5

Slide 5

Mobile websites were less usable

Slide 6

Slide 6

Websites were less capable Image from h

Slide 7

Slide 7

Slide 8

Slide 8

r ffl ine suppo

Slide 9

Slide 9

On mobile, people spent more time on apps compared to the web Graph from h

Slide 10

Slide 10

fi “mobile- rst” apps

Slide 11

Slide 11

Websites in 2016 Mobile Apps in 2016 ✅ Broad reach ✅ More usable ✅ More capable ✅ Easier to access ffl ✅ Available o ine

Slide 12

Slide 12

Image from h

Slide 13

Slide 13

rt tt tt ps://developers.google.com/web/updates/2015/12/ge ing-sta ed-pwa#what_is_a_progressive_web_app

Slide 14

Slide 14

Image from h

Slide 15

Slide 15

Slide 16

Slide 16

Slide 17

Slide 17

Slide 18

Slide 18

Slide 19

Slide 19

Slide 20

Slide 20

Slide 21

Slide 21

Slide 22

Slide 22

and installable PWA

Slide 23

Slide 23

How did PWAs fare in 2016?

Slide 24

Slide 24

Capable Reliable Installable 🤔

Slide 25

Slide 25

Critical capabilities Push noti cations & badging Background sync & fetch tt fi fi Access to device - les, contacts, se ings, etc.

Slide 26

Slide 26

Slide 27

Slide 27

Slide 28

Slide 28

Slide 29

Slide 29

if (awesomeFeature in window) { // do awesome thing } else { // same old thing }

Slide 30

Slide 30

if (awesomeFeature in window) { // do awesome thing } else { // same old thing }

Slide 31

Slide 31

Capable Reliable Installable ☹

Slide 32

Slide 32

Capable ☹ Reliable 🤔 Installable

Slide 33

Slide 33

A service worker is a script that your browser runs in the background, separate from a web page, opening the door to features that don’t need a web page or user interaction tt h ps://developers.google.com/web/fundamentals/primers/service-workers

Slide 34

Slide 34

Image from h

Slide 35

Slide 35

Slide 36

Slide 36

Slide 37

Slide 37

Slide 38

Slide 38

Slide 39

Slide 39

Slide 40

Slide 40

ace and when cached o ine can ensure instant, reliably good pe ormance to users on repeat visits. This means the application shell is not loaded from the network every time the user visits. Only the necessary content is needed from the network. ffl rf rf tt h ps://developers.google.com/web/fundamentals/architecture/app-shell

Slide 41

Slide 41

Slide 42

Slide 42

App shell <main> <h2>Latest Articles</h2> <section id=”excerpts”> <!— loading icon — > </section> </main>

Slide 43

Slide 43

Respond with cached page self.addEventListener(‘fetch’, (event) { event.respondWith( caches.match(event.request) .then((cachedResponse) );

=

= }); cachedResponse || fetch(event.request))

Slide 44

Slide 44

Add content to app shell if ( ‘serviceWorker’ in navigator ) { getArticlesFromIndexedDB() .then((articles) .then(() displayArticles(articles)) updateArticlesInBackground()); } else { fetchArticlesFromNetwork() .then((articles)

=

= } displayArticles(articles));

Slide 45

Slide 45

Add content to app shell if ( ‘serviceWorker’ in navigator ) { getArticlesFromIndexedDB() .then((articles) .then(() displayArticles(articles)) updateArticlesInBackground()); } else { fetchArticlesFromNetwork() .then((articles)

=

= } displayArticles(articles));

Slide 46

Slide 46

Add content to app shell if ( ‘serviceWorker’ in navigator ) { getArticlesFromIndexedDB() .then((articles) .then(() displayArticles(articles)) updateArticlesInBackground()); } else { fetchArticlesFromNetwork() .then((articles)

=

= } displayArticles(articles));

Slide 47

Slide 47

🤔

Slide 48

Slide 48

Capable ☹ Reliable ☹ Installable

Slide 49

Slide 49

Capable ☹ Reliable ☹ Installable 🤔

Slide 50

Slide 50

Slide 51

Slide 51

Requirements for “Add to Home Screen” 1. A manifest.json le 2. A service worker fi 3. Visit frequency heuristics 🤔

Slide 52

Slide 52

Add to Home Screen !== Install

Slide 53

Slide 53

Capable ☹ Reliable ☹ Installable ☹

Slide 54

Slide 54

fi PWAs ve years on

Slide 55

Slide 55

Slide 56

Slide 56

~1,500,000 websites may be installable on mobile home screens, o ering an app experience .dev/pwa-2021 ff rt fi tt h ps://

Slide 57

Slide 57

Capable Reliable Installable 🤔

Slide 58

Slide 58

Slide 59

Slide 59

Slide 60

Slide 60

Project Fugu is an e o to close gaps in the web’s capabilities by enabling new classes of applications to run on the web rt ff tt h p://www.chromium.org/teams/web-capabilities-fugu

Slide 61

Slide 61

Image from h

Slide 62

Slide 62

Slide 63

Slide 63

Critical capabilities cations & badging Background sync & fetch tt fi fi Access to device - les, contacts, se ings, etc.

Slide 64

Slide 64

Image from h

Slide 65

Slide 65

APIs cations API — display of noti cation message Push API — allows browsers to receive messages from a server fi fi App Badging API — display badge on app icon

Slide 66

Slide 66

Request permission to display push noti cations if (‘Notification’ in window) { Notification.requestPermission(); fi }

Slide 67

Slide 67

Display noti cation navigator.serviceWorker.ready.then((registration) registration.showNotification( ‘Hello AEA!’, { body: ‘How are you doing?’ } );

= fi }); {

Slide 68

Slide 68

Image from h

Slide 69

Slide 69

Get and send PushSubscription to server const pushSubscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: VAPID key }); / * * / saveToServer(pushSubscription);

Slide 70

Slide 70

Get and send PushSubscription to server const registration = await navigator.serviceWorker.register(‘/service-worker.js’); const pushSubscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: VAPID key }); / * * / saveToServer(pushSubscription);

Slide 71

Slide 71

Get and send PushSubscription to server const registration = await navigator.serviceWorker.register(‘/service-worker.js’); const pushSubscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: VAPID key }); / * * / saveToServer(pushSubscription);

Slide 72

Slide 72

Image from h

Slide 73

Slide 73

Receive noti cation in service worker fi });

Slide 74

Slide 74

Display noti cation self.addEventListener(‘push’, function(event) { event.waitUntil( self.registration.showNotification( ‘Hello AEA!’, { body: ‘How are you doing?’ } ) ); fi });

Slide 75

Slide 75

Set app badge navigator.setAppBadge(badgeNumber); navigator.clearAppBadge();

Slide 76

Slide 76

Set app badge navigator.setAppBadge(badgeNumber); navigator.clearAppBadge();

Slide 77

Slide 77

Set app badge navigator.setAppBadge(badgeNumber); navigator.clearAppBadge();

Slide 78

Slide 78

Slide 79

Slide 79

*⃣

Slide 80

Slide 80

h

Slide 81

Slide 81

Slide 82

Slide 82

Slide 83

Slide 83

APIs Background Fetch API — pe orm fetches in the background rf Periodic Background Sync API — sync data in the background

Slide 84

Slide 84

Slide 85

Slide 85

Register a sync task navigator.serviceWorker.ready.then(async (registration) await registration.sync.register(‘send-chat-message’);

= }); {

Slide 86

Slide 86

Detect connectivity self.addEventListener(‘sync’, function(event) { });

Slide 87

Slide 87

Complete task self.addEventListener(‘sync’, function(event) { if (event.tag ‘send-chat-message’) { event.waitUntil( send chat message } / * * /

= }); );

Slide 88

Slide 88

Slide 89

Slide 89

Sta a background fetch navigator.serviceWorker.ready.then(async (registration) { const videoFetch = await registration.backgroundFetch.fetch(‘video-fetch’, [‘/video.mp4’], { title: ‘Funny Video’, icons: [{ sizes: ‘300x300’, src: ‘/thumbnail.png’, type: ‘image/png’, }], downloadTotal: 60 * 1024 * 1024, });

= rt });

Slide 90

Slide 90

Sta a background fetch navigator.serviceWorker.ready.then(async (registration) { const videoFetch = await registration.backgroundFetch.fetch(‘video-fetch’, [‘/video.mp4’], { title: ‘Funny Video’, icons: [{ sizes: ‘300x300’, src: ‘/thumbnail.png’, type: ‘image/png’, }], downloadTotal: 60 * 1024 * 1024, });

= rt });

Slide 91

Slide 91

Sta a background fetch navigator.serviceWorker.ready.then(async (registration) { const videoFetch = await registration.backgroundFetch.fetch(‘video-fetch’, [‘/video.mp4’], { title: ‘Funny Video’, icons: [{ sizes: ‘300x300’, src: ‘/thumbnail.png’, type: ‘image/png’, }], downloadTotal: 60 * 1024 * 1024, });

= rt });

Slide 92

Slide 92

Track progress videoFetch.addEventListener(‘progress’, () { const percent = Math.round(videoFetch.downloaded / videoFetch.downloadTotal * 100); console.log(Download progress: ${percent}%);

= });

Slide 93

Slide 93

Service worker events self.addEventListener(‘backgroundfetchsuccess’ () ); self.addEventListener(‘backgroundfetchfailure’ () ); / / * * / / * * * * / / * *

/ /

=

);

self.addEventListener(‘backgroundfetchclick’ ()

);

self.addEventListener(‘backgroundfetchabort’ ()

Slide 94

Slide 94

Slide 95

Slide 95

Register a periodic sync task navigator.serviceWorker.ready.then(async (registration) await registration.periodicSync.register(‘chat-sync’, { minInterval: 24 * 60 * 60 * 1000, // Once a day });

= }); {

Slide 96

Slide 96

Register a periodic sync task navigator.serviceWorker.ready.then(async (registration) await registration.periodicSync.register(‘chat-sync’, { minInterval: 24 * 60 * 60 * 1000, // Once a day });

= }); {

Slide 97

Slide 97

Register a periodic sync task navigator.serviceWorker.ready.then(async (registration) await registration.periodicSync.register(‘chat-sync’, { minInterval: 24 * 60 * 60 * 1000, // Once a day });

= }); {

Slide 98

Slide 98

Detect periodic sync event self.addEventListener(‘periodicsync’, function(event) { });

Slide 99

Slide 99

Complete task self.addEventListener(‘periodicsync’, function(event) { if (event.tag ‘chat-sync’) { event.waitUntil( fetch and update chat messages } / * * /

= }); );

Slide 100

Slide 100

Slide 101

Slide 101

Slide 102

Slide 102

Slide 103

Slide 103

Image from h

Slide 104

Slide 104

(Some) APIs Web Share API & Web Share Target API File System Access API

Slide 105

Slide 105

Contact Picker API if (‘contacts’ in navigator) { }

Slide 106

Slide 106

Contact Picker API if (‘contacts’ in navigator) { const contacts = await navigator.contacts.select( [‘name’, ‘email’, ‘tel’, ‘icon’], {multiple: true} ); }

Slide 107

Slide 107

Slide 108

Slide 108

Web Share API if (navigator.share) { }

Slide 109

Slide 109

Web Share API if (navigator.share) { navigator.share({ title: ‘An Event Apart Spring Summit’, text: ‘Check out An Event Apart\’s latest conference!’, url: ‘https://aneventapart.com/event/spring-summit-2021’, }); }

Slide 110

Slide 110

Web Share Target API “share_target”: { “action”: “/share-target.html”, “method”: “GET”, “params”: { “title”: “title”, “text”: “text”, “url”: “url” } }

Slide 111

Slide 111

Web Share Target API window.addEventListener(‘DOMContentLoaded’, () { const { searchParams } = new URL(window.location); const title = searchParams.get(‘title’); const text = searchParams.get(‘text’); const url = searchParams.get(‘url’);

= });

Slide 112

Slide 112

Slide 113

Slide 113

Web Share Target

Slide 114

Slide 114

File System Access API if (‘showOpenFilePicker’ in window) { }

Slide 115

Slide 115

Read a local le let fileHandle; document.getElementById(‘open-file-picker’).addEventListener(‘click’, async () [fileHandle] = await window.showOpenFilePicker(); const file = await fileHandle.getFile(); const contents = await file.text();

= fi }); {

Slide 116

Slide 116

Read a local le let fileHandle; document.getElementById(‘open-file-picker’).addEventListener(‘click’, async () [fileHandle] = await window.showOpenFilePicker(); const file = await fileHandle.getFile(); const contents = await file.text();

= fi }); {

Slide 117

Slide 117

Read a local le let fileHandle; document.getElementById(‘open-file-picker’).addEventListener(‘click’, async () [fileHandle] = await window.showOpenFilePicker(); const file = await fileHandle.getFile(); const contents = await file.text();

= fi }); {

Slide 118

Slide 118

Read a local le let fileHandle; document.getElementById(‘open-file-picker’).addEventListener(‘click’, async () [fileHandle] = await window.showOpenFilePicker(); const file = await fileHandle.getFile(); const contents = await file.text();

= fi }); {

Slide 119

Slide 119

Save to a local le document.getElementById(‘save-file’).addEventListener(‘click’, async () const writable = await fileHandle.createWritable(); await writable.write( new contents ); await writable.close();

= / * fi * / }); {

Slide 120

Slide 120

Save to a local le document.getElementById(‘save-file’).addEventListener(‘click’, async () const writable = await fileHandle.createWritable(); await writable.write( new contents ); await writable.close();

= / * fi * / }); {

Slide 121

Slide 121

Save to a local le document.getElementById(‘save-file’).addEventListener(‘click’, async () const writable = await fileHandle.createWritable(); await writable.write( new contents ); await writable.close();

= / * fi * / }); {

Slide 122

Slide 122

Save to a local le document.getElementById(‘save-file’).addEventListener(‘click’, async () const writable = await fileHandle.createWritable(); await writable.write( new contents ); await writable.close();

= / * fi * / }); {

Slide 123

Slide 123

Slide 124

Slide 124

Slide 125

Slide 125

tt h ps://fugu-tracker.web.app/

Slide 126

Slide 126

Capable Reliable Installable 🙂

Slide 127

Slide 127

Capable 🙂 Reliable 🤔 Installable

Slide 128

Slide 128

Image from h

Slide 129

Slide 129

Slide 130

Slide 130

Slide 131

Slide 131

Slide 132

Slide 132

tt ps://developers.google.com/web/tools/workbox/guides/common-recipes

Slide 133

Slide 133

Cache JS & CSS import {registerRoute} from ‘workbox-routing’; import {StaleWhileRevalidate} from ‘workbox-strategies’; registerRoute( ({request}) request.destination ‘script’ || request.destination ‘style’, new StaleWhileRevalidate()

=

=

=

= );

Slide 134

Slide 134

Cache JS & CSS import {registerRoute} from ‘workbox-routing’; import {StaleWhileRevalidate} from ‘workbox-strategies’; registerRoute( ({request}) request.destination ‘script’ || request.destination ‘style’, new StaleWhileRevalidate()

=

=

=

= );

Slide 135

Slide 135

Cache JS & CSS import {registerRoute} from ‘workbox-routing’; import {StaleWhileRevalidate} from ‘workbox-strategies’; registerRoute( ({request}) request.destination ‘script’ || request.destination ‘style’, new StaleWhileRevalidate()

=

=

=

= );

Slide 136

Slide 136

Cache JS & CSS import {registerRoute} from ‘workbox-routing’; import {StaleWhileRevalidate} from ‘workbox-strategies’; registerRoute( ({request}) request.destination ‘script’ || request.destination ‘style’, new StaleWhileRevalidate()

=

=

=

= );

Slide 137

Slide 137

O ine Google Analytics import * as googleAnalytics from ‘workbox-google-analytics’; ffl googleAnalytics.initialize();

Slide 138

Slide 138

O ine Google Analytics import * as googleAnalytics from ‘workbox-google-analytics’; ffl googleAnalytics.initialize();

Slide 139

Slide 139

O ine Google Analytics import * as googleAnalytics from ‘workbox-google-analytics’; ffl googleAnalytics.initialize();

Slide 140

Slide 140

Cache o ine page const CACHE_NAME = ‘offline-html’; const FALLBACK_HTML_URL = ‘/offline.html’; self.addEventListener(‘install’, async (event) { event.waitUntil( caches.open(CACHE_NAME).then((cache) );

=

= ffl }); cache.add(FALLBACK_HTML_URL))

Slide 141

Slide 141

Cache o ine page const CACHE_NAME = ‘offline-html’; const FALLBACK_HTML_URL = ‘/offline.html’; self.addEventListener(‘install’, async (event) { event.waitUntil( caches.open(CACHE_NAME).then((cache) );

=

= ffl }); cache.add(FALLBACK_HTML_URL))

Slide 142

Slide 142

Cache o ine page const CACHE_NAME = ‘offline-html’; const FALLBACK_HTML_URL = ‘/offline.html’; self.addEventListener(‘install’, async (event) { event.waitUntil( caches.open(CACHE_NAME).then((cache) );

=

= ffl }); cache.add(FALLBACK_HTML_URL))

Slide 143

Slide 143

Cache o ine page const CACHE_NAME = ‘offline-html’; const FALLBACK_HTML_URL = ‘/offline.html’; self.addEventListener(‘install’, async (event) { event.waitUntil( caches.open(CACHE_NAME).then((cache) );

=

= ffl }); cache.add(FALLBACK_HTML_URL))

Slide 144

Slide 144

Serve o ine page const networkOnly = new NetworkOnly(); const navigationHandler = async (params) { try { return await networkOnly.handle(params); } catch (error) { return caches.match(FALLBACK_HTML_URL, { cacheName: CACHE_NAME }); } };

= ffl registerRoute( new NavigationRoute(navigationHandler) );

Slide 145

Slide 145

Serve o ine page const networkOnly = new NetworkOnly(); const navigationHandler = async (params) { try { return await networkOnly.handle(params); } catch (error) { return caches.match(FALLBACK_HTML_URL, { cacheName: CACHE_NAME }); } };

= ffl registerRoute( new NavigationRoute(navigationHandler) );

Slide 146

Slide 146

Serve o ine page const networkOnly = new NetworkOnly(); const navigationHandler = async (params) { try { return await networkOnly.handle(params); } catch (error) { return caches.match(FALLBACK_HTML_URL, { cacheName: CACHE_NAME }); } };

= ffl registerRoute( new NavigationRoute(navigationHandler) );

Slide 147

Slide 147

Serve o ine page const networkOnly = new NetworkOnly(); const navigationHandler = async (params) { try { return await networkOnly.handle(params); } catch (error) { return caches.match(FALLBACK_HTML_URL, { cacheName: CACHE_NAME }); } };

= ffl registerRoute( new NavigationRoute(navigationHandler) );

Slide 148

Slide 148

Serve o ine page const networkOnly = new NetworkOnly(); const navigationHandler = async (params) { try { return await networkOnly.handle(params); } catch (error) { return caches.match(FALLBACK_HTML_URL, { cacheName: CACHE_NAME }); } };

= ffl registerRoute( new NavigationRoute(navigationHandler) );

Slide 149

Slide 149

Slide 150

Slide 150

Slide 151

Slide 151

🤩

Slide 152

Slide 152

tt h ps://angular.io/guide/app-shell

Slide 153

Slide 153

tt h ps://create-react-app.dev/docs/making-a-progressive-web-app/

Slide 154

Slide 154

tt h ps://cli.vuejs.org/core-plugins/pwa.html

Slide 155

Slide 155

Capable 🙂 Reliable 😀 Installable

Slide 156

Slide 156

Capable 🙂 Reliable 😀 Installable 🤔

Slide 157

Slide 157

Slide 158

Slide 158

Slide 159

Slide 159

Requirements for “Add to Home Screen”

Slide 160

Slide 160

Requirements for “Add to Home Screen” 1. Web app manifest { “name”: “PWA in 2021”, “short_name”: “PWA in 2021”, “description”: “A demo of what we can do with PWAs in 2021”, “scope”: “/”, “display”: “standalone”, “background_color”: “#ffff”, “theme_color”: “#D33257”, “start_url”: “/”, }

Slide 161

Slide 161

tt h ps://tomitm.github.io/appmanifest/

Slide 162

Slide 162

Requirements for “Add to Home Screen” 1. Web app manifest 2. HTTPS

Slide 163

Slide 163

Requirements for “Add to Home Screen” 1. Web app manifest 2. HTTPS 3. Service worker (which returns a 200 ffl response when o ine)

Slide 164

Slide 164

Requirements for “Add to Home Screen” 1. Web app manifest 2. HTTPS 3. Service worker (which returns a response when o ine) * ffl 4. User engagement heuristic

Slide 165

Slide 165

fl More control over “Install” ow

Slide 166

Slide 166

Detect if PWA can be installed window.addEventListener(‘beforeinstallprompt’, (e)

= }); {

Slide 167

Slide 167

Save install prompt event let installPrompt; window.addEventListener(‘beforeinstallprompt’, (e) e.preventDefault(); installPrompt = e;

= }); {

Slide 168

Slide 168

Save install prompt event let installPrompt; window.addEventListener(‘beforeinstallprompt’, (e) e.preventDefault(); installPrompt = e;

= }); {

Slide 169

Slide 169

Show a custom install prompt let installPrompt; window.addEventListener(‘beforeinstallprompt’, (e) e.preventDefault(); installPrompt = e; showCustomInstallPrompt();

= }); {

Slide 170

Slide 170

Trigger install prompt customInstallButton.addEventListener(‘click’, async (e) installPrompt.prompt(); const { outcome } = await installPrompt.userChoice;

= }); {

Slide 171

Slide 171

Trigger install prompt customInstallButton.addEventListener(‘click’, async (e) installPrompt.prompt(); const { outcome } = await installPrompt.userChoice;

= }); {

Slide 172

Slide 172

Cleanup window.addEventListener(‘appinstalled’, ()

= }); {

Slide 173

Slide 173

Slide 174

Slide 174

Add to Homescreen == Install

Slide 175

Slide 175

Slide 176

Slide 176

A WebAPK is an Android Application Package (APK) automatically generated from a PWA and installed to the device.

Slide 177

Slide 177

fi Bene ts of WebAPK

Slide 178

Slide 178

Bene ts of WebAPK fi ✅ In the app drawer

Slide 179

Slide 179

Bene ts of WebAPK ✅ In the app drawer tt fi ✅ In app se ings

Slide 180

Slide 180

Bene ts of WebAPK ✅ In the app drawer ✅ In app se ings fi tt fi ✅ Includes intent lters

Slide 181

Slide 181

Image from h

Slide 182

Slide 182

is a way to open your PWA from your Android app using a protocol based on Custom Tabs tt h ps://developers.google.com/web/android/trusted-web-activity

Slide 183

Slide 183

Slide 184

Slide 184

Slide 185

Slide 185

Slide 186

Slide 186

tt h ps://github.com/GoogleChromeLabs/bubblewrap

Slide 187

Slide 187

Capable 🙂 Reliable 😀 Installable 😀

Slide 188

Slide 188

What’s next?

Slide 189

Slide 189

Capable 🤩 Reliable 🤩 Installable 🤩

Slide 190

Slide 190

(Some) Upcoming APIs Local font access Tabbed application mode Detecting orientation change events Widgets Detect/block screenshots Install-time permissions Splash screen

Slide 191

Slide 191

Web on Web on

Slide 192

Slide 192

Image from h

Slide 193

Slide 193

Slide 194

Slide 194

COO & VP Engineering of BuyCoins Google Web Expe ireaderinokun.com bitsofco.de rt @ireaderinokun