PWAs in 2021 Ire Aderinokun @ All Day Hey 2021

Mobile websites were less usable

Websites were less capable Image from h

r ffl ine suppo

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

fi “mobile- rst” apps

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

Image from h

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

Image from h

and installable PWA

How did PWAs fare in 2016?

Capable Reliable Installable 🤔

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

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

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

Capable Reliable Installable ☹

Capable ☹ Reliable 🤔 Installable

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

Image from h

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

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

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

=

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

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

=

= } displayArticles(articles));

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

=

= } displayArticles(articles));

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

=

= } displayArticles(articles));

🤔

Capable ☹ Reliable ☹ Installable

Capable ☹ Reliable ☹ Installable 🤔

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

Add to Home Screen !== Install

Capable ☹ Reliable ☹ Installable ☹

fi PWAs ve years on

~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://

Capable Reliable Installable 🤔

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

Image from h

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

Image from h

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

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

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

= fi }); {

Image from h

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

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);

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);

Image from h

Receive noti cation in service worker fi });

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

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

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

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

*⃣

h

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

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

= }); {

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

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

= }); );

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 });

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 });

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 });

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

= });

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

/ /

=

);

self.addEventListener(‘backgroundfetchclick’ ()

);

self.addEventListener(‘backgroundfetchabort’ ()

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 });

= }); {

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 });

= }); {

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 });

= }); {

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

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

= }); );

Image from h

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

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

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

Web Share API if (navigator.share) { }

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’, }); }

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

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’);

= });

Web Share Target

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

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 }); {

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 }); {

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 }); {

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 }); {

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 * / }); {

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 * / }); {

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 * / }); {

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 * / }); {

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

Capable Reliable Installable 🙂

Capable 🙂 Reliable 🤔 Installable

Image from h

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

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

=

=

=

= );

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

=

=

=

= );

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

=

=

=

= );

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

=

=

=

= );

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

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

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

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))

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))

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))

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))

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) );

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) );

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) );

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) );

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) );

🤩

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

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

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

Capable 🙂 Reliable 😀 Installable

Capable 🙂 Reliable 😀 Installable 🤔

Requirements for “Add to Home Screen”

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”: “/”, }

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

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

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

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

fl More control over “Install” ow

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

= }); {

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

= }); {

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

= }); {

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

= }); {

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

= }); {

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

= }); {

Cleanup window.addEventListener(‘appinstalled’, ()

= }); {

Add to Homescreen == Install

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

fi Bene ts of WebAPK

Bene ts of WebAPK fi ✅ In the app drawer

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

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

Image from h

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

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

Capable 🙂 Reliable 😀 Installable 😀

What’s next?

Capable 🤩 Reliable 🤩 Installable 🤩

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

Web on Web on

Image from h

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