Getting Started with PWAs [Workshop]

A presentation at PWA Summit in October 2021 in by Aaron Gustafson

Slide 1

Slide 1

Getting started with Progressive Web Apps Aaron Gustafson @AaronGustafson noti.st/AaronGustafson

Slide 2

Slide 2

@AaronGustafson Follow along ๏ Slides: https://aka.ms/pwasummit-workshop-slides ๏ Work files: https://aka.ms/pwasummit-workshop-code ๏ Final Demo: https://aka.ms/pwasummit-workshop-live

Slide 3

Slide 3

What exactly is a PWA?

Slide 4

Slide 4

What exactly is a Progressive Web App?

Slide 5

Slide 5

What exactly is a Progressive Web App?

Slide 6

Slide 6

“Progressive Progressive Web App” App is a marketing term

Slide 7

Slide 7

Progressive Web App

Slide 8

Slide 8

Game Gallery Book Progressive Web App Newspaper Art Project Tool

Slide 9

Slide 9

Progressive Web Site

Slide 10

Slide 10

Who’s behind PWAs?

Slide 11

Slide 11

@AaronGustafson What’s a PWA, technically? HTTPS Web App Manifest Service Worker

Slide 12

Slide 12

Should I believe the hype?

Slide 13

Slide 13

Maybe?

Slide 14

Slide 14

Starbucks: 2x increase in daily active users desktop == mobile aka.ms/google-io-2018

Slide 15

Slide 15

Tinder: Core experience with 90% less code aka.ms/tinder-pwa-2017

Slide 16

Slide 16

Uber: Core PWA in 50k* Loads in under 3s over 2G * gzipped aka.ms/uber-pwa-2021

Slide 17

Slide 17

Forbes: 61% improvement in load time on mobile aka.ms/forbes-pwa-2021

Slide 18

Slide 18

Trivago: 150% increase in use when added to the homescreen aka.ms/trivago-pwa-2017

Slide 19

Slide 19

West Elm: 15% increase in time on site 9% increase in revenue per visit aka.ms/west-elm-pwa-2017

Slide 20

Slide 20

Flipkart: 50% of new customers via PWA 60% of PWA users uninstalled their app aka.ms/flipkart-2021

Slide 21

Slide 21

Slide 22

Slide 22

PWAs start with a great web experience and then enhance that experience for performance, resilience, installation, and engagement

Slide 23

Slide 23

PWAs start with a great web experience and then enhance that experience for performance, resilience, installation, and engagement

Slide 24

Slide 24

Progressive Web App

Slide 25

Slide 25

Progressive Web App Enhancement

Slide 26

Slide 26

@AaronGustafson User Experience Enhance the experience Capabilities

Slide 27

Slide 27

Progressive /prəˈɡresiv/ happening or developing gradually or in stages; proceeding step by step

Slide 28

Slide 28

@AaronGustafson What’s a PWA, technically? HTTPS Web App Manifest Service Worker

Slide 29

Slide 29

@AaronGustafson Let’s talk about HTTPS

Slide 30

Slide 30

@AaronGustafson HTTPS is simple (& free) now ๏ ๏ Many hosts include it ๏ GitHub ๏ Netlify ๏ AWS ๏ etc. LetsEnrcypt & Certbot for everything else https://letsencrypt.org/

Slide 31

Slide 31

@AaronGustafson What’s a PWA, technically? HTTPS Web App Manifest Service Worker

Slide 32

Slide 32

@AaronGustafson Let’s talk about the Manifest

Slide 33

Slide 33

@AaronGustafson Manifest files are JSON files { “property_a”: “value”, “property_b”: [“value_1”, “value_2”], “property_c”: { “nested_property”: “nested_value” } }

Slide 34

Slide 34

@AaronGustafson Minimum Viable Manifest { “lang”: “en-US”, “name”: “Aaron Gustafson”, “start_url”: “/” }

Slide 35

Slide 35

@AaronGustafson Minimum Viable Manifest { “lang”: “en-US”, “name”: “Aaron Gustafson”, “start_url”: “/” }

Slide 36

Slide 36

@AaronGustafson Minimum Viable Manifest { “lang”: “en-US”, “dir”: “ltr”, “name”: “Aaron Gustafson”, “start_url”: “/” }

Slide 37

Slide 37

@AaronGustafson Minimum Viable Manifest { “lang”: “en-US”, “dir”: “auto”, // default “name”: “Aaron Gustafson”, “start_url”: “/” }

Slide 38

Slide 38

@AaronGustafson Minimum Viable Manifest { “lang”: “en-US”, “name”: “Aaron Gustafson”, “start_url”: “/” }

Slide 39

Slide 39

@AaronGustafson Minimum Viable Manifest { “lang”: “en-US”, “name”: “Aaron Gustafson”, “start_url”: “/” }

Slide 40

Slide 40

@AaronGustafson Minimum Viable Manifest { “lang”: “en-US”, “name”: “Aaron Gustafson”, “short_name”: “AaronG”, “start_url”: “/” }

Slide 41

Slide 41

@AaronGustafson Minimum Viable Manifest { “lang”: “en-US”, “name”: “Aaron Gustafson”, “short_name”: “AaronG”, “start_url”: “/” }

Slide 42

Slide 42

@AaronGustafson Minimum Viable Manifest { “lang”: “en-US”, “name”: “Aaron Gustafson”, “short_name”: “AaronG”, “start_url”: “/” }

Slide 43

Slide 43

@AaronGustafson Reference in the head <link rel=”manifest” href=”/manifest.json”>

Slide 44

Slide 44

@AaronGustafson Reference in the head <link rel=”manifest” href=”/manifest.json”>

Slide 45

Slide 45

@AaronGustafson Reference in the head <link rel=”manifest” href=”/manifest.json”> t s e f i n a m b e w . e m a n _ p p e a b o s l a d l u o w ) r a l i m i E s M I (or M s e r i u q e r t u b , e l b a t n p e o s c j + ac t s e f i n a m / n o i t a c i l p ap

Slide 46

Slide 46

@AaronGustafson Let‘s make a manifest! ๏ Open a new text document in your site and name it manifest.json ๏ Create a basic JSON object inside, including the following Manifest members: 1. lang (and, optionally, dir), 2. name, 3. short_name (if you need it), and 4. start_url.

Slide 47

Slide 47

How’d it go? Any questions?

Slide 48

Slide 48

@AaronGustafson Let’s prep for install… { “lang”: “en-US”, “name”: “Aaron Gustafson”, “short_name”: “AaronG”, “start_url”: “/” }

Slide 49

Slide 49

@AaronGustafson Start at the beginning { … “start_url”: “/” }

Slide 50

Slide 50

@AaronGustafson Where does this apply? { … “start_url”: “/”, “scope”: “/” // defaults to the start_url path }

Slide 51

Slide 51

@AaronGustafson What should it look like? { … “start_url”: “/”, “display”: “minimal-ui” }

Slide 52

Slide 52

@AaronGustafson Display Modes “display”: “browser” “display”: “standalone” “display”: “minimal-ui”

Slide 53

Slide 53

@AaronGustafson Display Modes “display”: “browser” “display”: “fullscreen” “standalone” “display”: “minimal-ui”

Slide 54

Slide 54

@AaronGustafson Need to lock orientation? { … “start_url”: “/”, “display”: “minimal-ui”, “orientation”: “any” }

Slide 55

Slide 55

@AaronGustafson Orientation options ๏ “any “– no preference ๏ “natural” – the default orientation of the device ๏ “portrait” ๏ ๏ “portrait-primary” ๏ “portrait-secondary” “landscape” ๏ “landscape-primary” ๏ “landscape-secondary” Details: https://www.w3.org/TR/screen-orientation/

Slide 56

Slide 56

@AaronGustafson A little color… { … “start_url”: “/”, “display”: “minimal-ui”, “orientation”: “any”, “theme_color”: “#27831B” }

Slide 57

Slide 57

@AaronGustafson A little color… { … “start_url”: “/”, “display”: “minimal-ui”, “orientation”: “any”, “theme_color”: “#27831B”, “background_color”: “#fffcf4” }

Slide 58

Slide 58

@AaronGustafson Adding icons { … “icons”: [ { “src”: “/i/og-logo.png”, “type”: “image/png”, “sizes”: “800x600” }, { “src”: “/i/notification-icon.png”, “type”: “image/png”, “sizes”: “256x256” }, { “src”: “/favicon.png”, “type”: “image/png”, “sizes”: “16x16” } ] }

Slide 59

Slide 59

@AaronGustafson Adding icons { … “icons”: [ { “src”: “/i/og-logo.png”, “type”: “image/png”, “sizes”: “800x600” }, { “src”: “/i/notification-icon.png”, “type”: “image/png”, “sizes”: “256x256” }, { “src”: “/favicon.png”, “type”: “image/png”, “sizes”: “16x16” } ] }

Slide 60

Slide 60

@AaronGustafson Anatomy of an ImageResource { “src”: “/i/og-logo.png”, “type”: “image/png”, “sizes”: “800x600” }

Slide 61

Slide 61

@AaronGustafson Anatomy of an ImageResource { “src”: “/i/og-logo.png”, “type”: “image/png”, “sizes”: “800x600” }

Slide 62

Slide 62

@AaronGustafson Anatomy of an ImageResource { “src”: “/i/og-logo.png”, “type”: “image/png”, “sizes”: “800x600” }

Slide 63

Slide 63

@AaronGustafson Anatomy of an ImageResource { “src”: “/i/og-logo.png”, “type”: “image/png”, “sizes”: “800x600” }

Slide 64

Slide 64

@AaronGustafson Anatomy of an ImageResource { “src”: “/i/og-logo.png”, “type”: “image/png”, “sizes”: “800x600” }

Slide 65

Slide 65

@AaronGustafson Anatomy of an ImageResource { “src”: “/i/og-logo.png”, “type”: “image/png”, “sizes”: “800x600”, “purpose”: } S O e h t l l i e f l b “monochrome” d i a l n o e s o a t r t e n v a o ) w o e g g u o d o l a y r b f u I a o y r o k f s a s a m h c to (su

Slide 66

Slide 66

@AaronGustafson Anatomy of an ImageResource { “src”: “/i/og-logo.png”, “type”: “image/png”, “sizes”: “800x600”, “purpose”: “maskable” } d e n g i s e d d n s i a s e k g s a a im m d n n i o c m i n h i t i e w n o z e f a s

Slide 67

Slide 67

@AaronGustafson Let‘s improve our manifest! ๏ Add the following Manifest members: 1. display, 2. theme_color, 3. background_color, and 4. icons Suggested sizes: 48x48, 72x72, 96x96, 144x144, 192x192, and 512x512

Slide 68

Slide 68

@AaronGustafson User Experience Enhance the experience Capabilities

Slide 69

Slide 69

@AaronGustafson User Experience Enhance the experience Capabilities

Slide 70

Slide 70

How’d it go? Any questions?

Slide 71

Slide 71

@AaronGustafson What’s a PWA, technically? HTTPS Web App Manifest Service Worker

Slide 72

Slide 72

@AaronGustafson Let’s talk about Service Worker

Slide 73

Slide 73

@AaronGustafson Registering a Service Worker if ( “serviceWorker” in navigator ) { navigator.serviceWorker.register( “/serviceworker.min.js” ); }

Slide 74

Slide 74

@AaronGustafson Registering a Service Worker if ( “serviceWorker” in navigator ) { navigator.serviceWorker.register( “/serviceworker.min.js” ); }

Slide 75

Slide 75

@AaronGustafson Registering a Service Worker if ( “serviceWorker” in navigator ) { navigator.serviceWorker.register( “/serviceworker.min.js” ); }

Slide 76

Slide 76

@AaronGustafson Registering a Service Worker if ( “serviceWorker” in navigator ) { navigator.serviceWorker.register( “/serviceworker.min.js” ); } s i h t Pa o p im ! t n a rt

Slide 77

Slide 77

@AaronGustafson Registering a Service Worker if ( “serviceWorker” in navigator ) { navigator.serviceWorker.register( “/serviceworker.min.js” ) .then(function( registration ){ console.log( “Success!”, registration.scope ); }) .catch(function( error ){ console.error( “Failure!” , error ); }); }

Slide 78

Slide 78

@AaronGustafson User Experience Enhance the experience Capabilities

Slide 79

Slide 79

@AaronGustafson The Service Worker Lifecycle Browser Install Activation aka.ms/pwa-lifecycle Ready

Slide 80

Slide 80

@AaronGustafson Listening for these events self.addEventListener( “install”, function( event ){ console.log( “installing” ); });

Slide 81

Slide 81

@AaronGustafson Listening for these events self.addEventListener( “install”, function( event ){ console.log( “installing” ); });

Slide 82

Slide 82

@AaronGustafson Listening for these events self.addEventListener( “install”, function( event ){ console.log( “installing” ); });

Slide 83

Slide 83

@AaronGustafson Listening for these events self.addEventListener( “install”, function( event ){ console.log( “installing” ); });

Slide 84

Slide 84

@AaronGustafson Let‘s make a Service Worker! ๏ Create a new file for your service worker ๏ Register the Service Worker navigator.serviceWorker.register( path ) ๏ Log to the console from the following events: ๏ install ๏ activate

Slide 85

Slide 85

@AaronGustafson Yours should look similar self.addEventListener( “install”, function( event ){ console.log( “installing” ); }); self.addEventListener( “activate”, function( event ){ console.log( “activating” ); });

Slide 86

Slide 86

@AaronGustafson The Service Worker Lifecycle Browser Install Activation aka.ms/pwa-lifecycle Ready

Slide 87

Slide 87

@AaronGustafson Preloading assets self.addEventListener( “install”, function( event ){ event.waitUntil( caches.open(“v1”).then(function(cache) { return cache.addAll([ “/css/main.css”, “/js/main.js” ]); }) ); });

Slide 88

Slide 88

@AaronGustafson Preloading assets self.addEventListener( “install”, function( event ){ event.waitUntil( caches.open(“v1”).then(function(cache) { return cache.addAll([ “/css/main.css”, “/js/main.js” ]); }) ); });

Slide 89

Slide 89

@AaronGustafson Preloading assets self.addEventListener( “install”, function( event ){ event.waitUntil( caches.open(“v1”).then(function(cache) { return cache.addAll([ “/css/main.css”, “/js/main.js” ]); }) ); });

Slide 90

Slide 90

@AaronGustafson Preloading assets self.addEventListener( “install”, function( event ){ event.waitUntil( caches.open(“v1”).then(function(cache) { return cache.addAll([ “/css/main.css”, “/js/main.js” ]); }) ); });

Slide 91

Slide 91

@AaronGustafson Preloading assets self.addEventListener( “install”, function( event ){ event.waitUntil( caches.open(“v1”).then(function(cache) { return cache.addAll([ “/css/main.css”, “/js/main.js” ]); }) ); });

Slide 92

Slide 92

@AaronGustafson Let’s refactor self.addEventListener( “install”, function( event ){ event.waitUntil( caches.open(“v1”).then(function(cache) { return cache.addAll([ “/css/main.css”, “/js/main.js” ]); }) ); });

Slide 93

Slide 93

@AaronGustafson Let’s refactor const VERSION = “v1”; self.addEventListener( “install”, function( event ){ event.waitUntil( caches.open( VERSION ).then(function(cache) { return cache.addAll([ “/css/main.css”, “/js/main.js” ]); }) ); });

Slide 94

Slide 94

@AaronGustafson Let‘s preload assets ๏ Leverage the install event to pre-load some assets ๏ Load your page in a browser and see that the assets are loaded ๏ Bump your version number and reload the page ๏ What happened?

Slide 95

Slide 95

@AaronGustafson Clean up after yourself const VERSION = “v1”; // install event self.addEventListener( “activate”, event => { // clean up stale caches event.waitUntil( caches.keys() .then( keys => { return Promise.all( keys.filter( key => { return ! key.startsWith( VERSION ); }) .map( key => { return caches.delete( key ); }) ); }) ); });

Slide 96

Slide 96

@AaronGustafson Clean up after yourself const VERSION = “v1”; // install event self.addEventListener( “activate”, event => { // clean up stale caches event.waitUntil( caches.keys() .then( keys => { return Promise.all( keys.filter( key => { return ! key.startsWith( VERSION ); }) .map( key => { return caches.delete( key ); }) ); }) ); });

Slide 97

Slide 97

@AaronGustafson Clean up after yourself const VERSION = “v1”; // install event self.addEventListener( “activate”, event => { // clean up stale caches event.waitUntil( caches.keys() .then( keys => { return Promise.all( keys.filter( key => { return ! key.startsWith( VERSION ); }) .map( key => { return caches.delete( key ); }) ); }) ); });

Slide 98

Slide 98

@AaronGustafson Clean up after yourself const VERSION = “v1”; // install event self.addEventListener( “activate”, event => { // clean up stale caches event.waitUntil( caches.keys() .then( keys => { return Promise.all( keys.filter( key => { return ! key.startsWith( VERSION ); }) .map( key => { return caches.delete( key ); }) ); }) ); });

Slide 99

Slide 99

@AaronGustafson Clean up after yourself const VERSION = “v1”; // install event self.addEventListener( “activate”, event => { // clean up stale caches event.waitUntil( caches.keys() .then( keys => { return Promise.all( keys.filter( key => { return ! key.startsWith( VERSION ); }) .map( key => { return caches.delete( key ); }) ); }) ); });

Slide 100

Slide 100

@AaronGustafson Clean up after yourself const VERSION = “v1”; // install event self.addEventListener( “activate”, event => { // clean up stale caches event.waitUntil( caches.keys() .then( keys => { return Promise.all( keys.filter( key => { return ! key.startsWith( VERSION ); }) .map( key => { return caches.delete( key ); }) ); }) ); });

Slide 101

Slide 101

@AaronGustafson Let‘s clean up ๏ Leverage the activate event to clear stale caches ๏ Load your page in a browser ๏ What happened?

Slide 102

Slide 102

@AaronGustafson Use the latest SW immediately const VERSION = “v1”; self.addEventListener( “install”, function( event ){ event.waitUntil( caches.open( VERSION ).then(function(cache) { return cache.addAll([ “/css/main.css”, “/js/main.js” ]); }) ); self.skipWaiting(); });

Slide 103

Slide 103

@AaronGustafson Claim any active clients self.addEventListener( “activate”, event => { // clean up stale caches event.waitUntil( caches.keys() .then( keys => { return Promise.all( keys.filter( key => { return ! key.startsWith( VERSION ); }) .map( key => { return caches.delete( key ); }) ); }) ); clients.claim(); });

Slide 104

Slide 104

@AaronGustafson Look what happens ๏ ๏ Add skipWaiting() to your install event ๏ Look at the DevTools and observe how the state of the Service Worker changes with and without this line of code. ๏ What happened? Open your site in two tabs and add clients.claim() to your activate event ๏ Look at the DevTools in each. ๏ What happened?

Slide 105

Slide 105

@AaronGustafson Talk about the network

Slide 106

Slide 106

@AaronGustafson How requests are made Browser Internet

Slide 107

Slide 107

@AaronGustafson Along comes Service Worker Browser Internet

Slide 108

Slide 108

@AaronGustafson Along comes Service Worker Browser Cache Internet

Slide 109

Slide 109

@AaronGustafson Along comes Service Worker Browser ! Cache Internet

Slide 110

Slide 110

@AaronGustafson Intercepting requests self.addEventListener( “fetch”, function( event ){ console.log( “fetching” ); });

Slide 111

Slide 111

@AaronGustafson Let‘s try it out ๏ ๏ Add a fetch event handler ๏ Load your page in a browser ๏ What happened? Instead of a string, log event.request.url ๏ Load your page in a browser ๏ What do you see?

Slide 112

Slide 112

@AaronGustafson We can issue our own fetch self.addEventListener( “fetch”, function( event ){ event.respondWith( fetch( event.request ) ); });

Slide 113

Slide 113

@AaronGustafson We can issue our own fetch self.addEventListener( “fetch”, function( event ){ event.respondWith( fetch( event.request ) ); });

Slide 114

Slide 114

What if the request fails?

Slide 115

Slide 115

@AaronGustafson What if the request fails? const VERSION = “v1”, OFFLINE_PAGE = “offline.html”;

Slide 116

Slide 116

@AaronGustafson What if the request fails? self.addEventListener( “install”, function( event ){ event.waitUntil( caches.open(“v1”).then(function(cache) { return cache.addAll([ “/css/main.css”, “/js/main.js”, OFFLINE_PAGE ]); }) ); });

Slide 117

Slide 117

@AaronGustafson What if the request fails? self.addEventListener( “fetch”, function( event ){ if ( event.request.mode === “navigate” ) { event.respondWith( fetch(event.request) .catch(error => { console.log( “Fetch failed; returning offline page.” ); return caches.match( OFFLINE_PAGE ); }) ); } });

Slide 118

Slide 118

@AaronGustafson What if the request fails? self.addEventListener( “fetch”, function( event ){ if ( event.request.mode === “navigate” ) { event.respondWith( fetch(event.request) .catch(error => { console.log( “Fetch failed; returning offline page.” ); return caches.match( OFFLINE_PAGE ); }) ); } });

Slide 119

Slide 119

@AaronGustafson What if the request fails? self.addEventListener( “fetch”, function( event ){ if ( event.request.mode === “navigate” ) { event.respondWith( fetch(event.request) .catch(error => { console.log( “Fetch failed; returning offline page.” ); return caches.match( OFFLINE_PAGE ); }) ); } });

Slide 120

Slide 120

@AaronGustafson What if the request fails? self.addEventListener( “fetch”, function( event ){ if ( event.request.mode === “navigate” ) { event.respondWith( fetch(event.request) .catch(error => { console.log( “Fetch failed; returning offline page.” ); return caches.match( OFFLINE_PAGE ); }) ); } });

Slide 121

Slide 121

@AaronGustafson Let‘s try it out ๏ Add an offline.html page ๏ Pre-cache it during install ๏ Remember to rev VERSION ๏ Add a fetch handler for navigations, providing the offline page as a fallback ๏ Turn off the network (once you know the SW is running) and see what happens

Slide 122

Slide 122

@AaronGustafson Caching strategies ๏ Network → cache → offline ๏ Cache → network → offline ๏ Cache vs network race → cache → offline ๏ etc.

Slide 123

Slide 123

@AaronGustafson Network, then cache if ( event.request.mode === “navigate” ) { event.respondWith( fetch( event.request ) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }

Slide 124

Slide 124

@AaronGustafson Network, then cache self.addEventListener( “fetch”, function( event ){ let request = event.request, url = request.url; // all the rest of the code }

Slide 125

Slide 125

@AaronGustafson Network, then cache if ( event.request.mode === “navigate” ) { event.respondWith( fetch( event.request ) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }

Slide 126

Slide 126

@AaronGustafson Network, then cache if ( request.mode === “navigate” ) { event.respondWith( fetch( request ) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }

Slide 127

Slide 127

@AaronGustafson Network, then cache if ( request.mode === “navigate” ) { event.respondWith( fetch( request ) .then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); }) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }

Slide 128

Slide 128

@AaronGustafson Network, then cache if ( request.mode === “navigate” ) { event.respondWith( fetch( request ) .then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); }) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }

Slide 129

Slide 129

@AaronGustafson Network, then cache if ( request.mode === “navigate” ) { event.respondWith( fetch( request ) .then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); }) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }

Slide 130

Slide 130

@AaronGustafson Network, then cache if ( request.mode === “navigate” ) { event.respondWith( fetch( request ) .then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); }) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }

Slide 131

Slide 131

@AaronGustafson Network, then cache .catch(error => { return caches.match( OFFLINE_PAGE ); })

Slide 132

Slide 132

@AaronGustafson Network, then cache .catch(error => { return caches.match( request ).then( cached_result => { if ( cached_result ) { return cached_result; } return caches.match( OFFLINE_PAGE ); }); })

Slide 133

Slide 133

@AaronGustafson Network, then cache .catch(error => { return caches.match( request ).then( cached_result => { if ( cached_result ) { return cached_result; } return caches.match( OFFLINE_PAGE ); }); })

Slide 134

Slide 134

@AaronGustafson Network, then cache .catch(error => { return caches.match( request ).then( cached_result => { if ( cached_result ) { return cached_result; } return caches.match( OFFLINE_PAGE ); }); })

Slide 135

Slide 135

@AaronGustafson Network, then cache if ( request.mode === “navigate” ) { event.respondWith( fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); }) .catch(error => { return caches.match( request ).then( cached_result => { if ( cached_result ) { return cached_result; } return caches.match( OFFLINE_PAGE ); }); }) ); }

Slide 136

Slide 136

@AaronGustafson Caching strategies ✓ Network → cache → offline ๏ Cache → network → offline ๏ Cache vs network race → cache → offline ๏ etc.

Slide 137

Slide 137

@AaronGustafson Cache first, then network if ( /.css$/.test(url) || /.js$/.test(url) ) { event.respondWith( caches.match( request ) .then( cached_result => { if ( cached_result ) { return cached_result; } return fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, reponse ); }) ); return response.clone(); }) .catch( new Response( “”, { status: 408, statusText: “The server appears to be offline.” }) ); }) ); }

Slide 138

Slide 138

@AaronGustafson Cache first, then network if ( /.css$/.test(url) || /.js$/.test(url) ) { event.respondWith( caches.match( request ) .then( cached_result => { if ( cached_result ) { return cached_result; } return fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, reponse ); }) ); return response.clone(); }) .catch( new Response( “”, { status: 408, statusText: “The server appears to be offline.” }) ); }) ); }

Slide 139

Slide 139

@AaronGustafson Cache first, then network if ( /.css$/.test(url) || /.js$/.test(url) ) { event.respondWith( caches.match( request ) .then( cached_result => { if ( cached_result ) { return cached_result; } return fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, reponse ); }) ); return response.clone(); }) .catch( new Response( “”, { status: 408, statusText: “The server appears to be offline.” }) ); }) ); }

Slide 140

Slide 140

@AaronGustafson Cache first, then network if ( /.css$/.test(url) || /.js$/.test(url) ) { event.respondWith( caches.match( request ) .then( cached_result => { if ( cached_result ) { return cached_result; } return fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, reponse ); }) ); return response.clone(); }) .catch( new Response( “”, { status: 408, statusText: “The server appears to be offline.” }) ); }) ); }

Slide 141

Slide 141

@AaronGustafson Cache first, then network if ( /.css$/.test(url) || /.js$/.test(url) ) { event.respondWith( caches.match( request ) .then( cached_result => { if ( cached_result ) { return cached_result; } return fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); }) .catch( new Response( “”, { status: 408, statusText: “The server appears to be offline.” }) ); }) ); }

Slide 142

Slide 142

@AaronGustafson Cache first, then network if ( /.css$/.test(url) || /.js$/.test(url) ) { event.respondWith( caches.match( request ) .then( cached_result => { if ( cached_result ) { return cached_result; } return fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, reponse ); }) ); return response.clone(); }) .catch( new Response( “”, { status: 408, statusText: “The server appears to be offline.” }) ); }) ); }

Slide 143

Slide 143

Anyone see an opportunity for refactoring?

Slide 144

Slide 144

@AaronGustafson We cached a fetch twice return fetch( request ).then( response => { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); })

Slide 145

Slide 145

@AaronGustafson Let’s make it a function function cacheResponse( response ) { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( request, response ); }) ); return response.clone(); }

Slide 146

Slide 146

@AaronGustafson Let’s make it a function function cacheResponse( response, event ) { event.waitUntil( caches.open( VERSION ).then( cache => { return cache.put( event.request, response ); }) ); return response.clone(); }

Slide 147

Slide 147

@AaronGustafson And we can use it like this return fetch( request ) .then( response => cacheResponse( response, event ) )

Slide 148

Slide 148

@AaronGustafson Network, then cache if ( request.mode === “navigate” ) { event.respondWith( fetch( request ) .then( response => cacheResponse( response, event ) ) .catch(error => { return caches.match( request ).then( cached_result => { if ( cached_result ) { return cached_result; } return caches.match( OFFLINE_PAGE ); }); }) ); }

Slide 149

Slide 149

@AaronGustafson Cache first, then network if ( /.css$/.test(url) || /.js$/.test(url) ) { event.respondWith( caches.match( request ) .then( cached_result => { if ( cached_result ) { return cached_result; } return fetch( request ) .then( response => cacheResponse( response, event ) ) .catch( new Response( “”, { status: 408, statusText: “The server appears to be offline.” }) ); }) ); }

Slide 150

Slide 150

@AaronGustafson Caching strategies ✓ Network → cache → offline ✓ Cache → network → offline ๏ Cache vs network race → cache → offline ๏ etc.

Slide 151

Slide 151

@AaronGustafson Who will win? document.addEventListener(‘DOMContentLoaded’, function(event) { var networkDone = false; var networkRequest = fetch(‘weather.json’).then(function(response) { return response.json(); }) .then(function(json) { networkDone = true; updatePage(json); }); caches.match(‘weather.json’).then(function(response) { if ( ! response) throw Error(‘No data’); return response.json(); }) .then(function(json) { if (!networkDone) updatePage(json); }) .catch(function() { return networkRequest; }) .catch(function() { console.log(‘We have nothing.’); }) .then(hideLoading); }); https://git.io/v56s4

Slide 152

Slide 152

@AaronGustafson Who will win? document.addEventListener(‘DOMContentLoaded’, function(event) { var networkDone = false; var networkRequest = fetch(‘weather.json’).then(function(response) { return response.json(); }) .then(function(json) { networkDone = true; updatePage(json); }); caches.match(‘weather.json’).then(function(response) { if ( ! response) throw Error(‘No data’); return response.json(); }) .then(function(json) { if (!networkDone) updatePage(json); }) .catch(function() { return networkRequest; }) .catch(function() { console.log(‘We have nothing.’); }) .then(hideLoading); }); https://git.io/v56s4

Slide 153

Slide 153

@AaronGustafson Who will win? document.addEventListener(‘DOMContentLoaded’, function(event) { var networkDone = false; var networkRequest = fetch(‘weather.json’).then(function(response) { return response.json(); }) .then(function(json) { networkDone = true; updatePage(json); }); caches.match(‘weather.json’).then(function(response) { if ( ! response) throw Error(‘No data’); return response.json(); }) .then(function(json) { if (!networkDone) updatePage(json); }) .catch(function() { return networkRequest; }) .catch(function() { console.log(‘We have nothing.’); }) .then(hideLoading); }); https://git.io/v56s4

Slide 154

Slide 154

@AaronGustafson Who will win? document.addEventListener(‘DOMContentLoaded’, function(event) { var networkDone = false; var networkRequest = fetch(‘weather.json’).then(function(response) { return response.json(); }) .then(function(json) { networkDone = true; updatePage(json); }); caches.match(‘weather.json’).then(function(response) { if ( ! response) throw Error(‘No data’); return response.json(); }) .then(function(json) { if (!networkDone) updatePage(json); }) .catch(function() { return networkRequest; }) .catch(function() { console.log(‘We have nothing.’); }) .then(hideLoading); }); https://git.io/v56s4

Slide 155

Slide 155

@AaronGustafson Let‘s discuss ๏ In what scenarios would these different caching strategies be most appropriate? ๏ Are there other strategies you’d like to discuss? ๏ Do you want to add these caching strategies to your site now?

Slide 156

Slide 156

Could we save data?

Slide 157

Slide 157

@AaronGustafson Look at the connection let slow_connection = false, save_data = false; function testConnection() { // only test every minute if ( last_tested && Date.now() < last_tested + ( 60 * 1000 ) ) { return; } if ( ‘connection’ in navigator ) { slow_connection = ( navigator.connection.downlink < 0.5 ); save_data = navigator.connection.saveData; last_tested = Date.now(); } }

Slide 158

Slide 158

@AaronGustafson Look at the connection let slow_connection = false, save_data = false; function testConnection() { // only test every minute if ( last_tested && Date.now() < last_tested + ( 60 * 1000 ) ) { return; } if ( ‘connection’ in navigator ) { slow_connection = ( navigator.connection.downlink < 0.5 ); save_data = navigator.connection.saveData; last_tested = Date.now(); } }

Slide 159

Slide 159

@AaronGustafson Look at the connection let slow_connection = false, save_data = false; function testConnection() { // only test every minute if ( last_tested && Date.now() < last_tested + ( 60 * 1000 ) ) { return; } if ( ‘connection’ in navigator ) { slow_connection = ( navigator.connection.downlink < 0.5 ); save_data = navigator.connection.saveData; last_tested = Date.now(); } }

Slide 160

Slide 160

@AaronGustafson Look at the connection let slow_connection = false, save_data = false; function testConnection() { // only test every minute if ( last_tested && Date.now() < last_tested + ( 60 * 1000 ) ) { return; } if ( ‘connection’ in navigator ) { slow_connection = ( navigator.connection.downlink < 0.5 ); save_data = navigator.connection.saveData; last_tested = Date.now(); } }

Slide 161

Slide 161

@AaronGustafson Look at the connection self.addEventListener( “fetch”, function( event ){ testConnection(); let request = event.request, url = request.url; … });

Slide 162

Slide 162

@AaronGustafson Use that information else if ( request.headers.get(“Accept”).includes(“image”) ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( // Respond with an “offline” image ); } else { // Respond with a “saving data” image } }) ); }

Slide 163

Slide 163

@AaronGustafson Use that information else if ( request.headers.get(“Accept”).includes(“image”) ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( // Respond with an “offline” image ); } else { // Respond with a “saving data” image } }) ); }

Slide 164

Slide 164

@AaronGustafson Use that information else if ( request.headers.get(“Accept”).includes(“image”) ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( // Respond with an “offline” image ); } else { // Respond with a “saving data” image } }) ); }

Slide 165

Slide 165

@AaronGustafson Use that information else if ( request.headers.get(“Accept”).includes(“image”) ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( // Respond with an “offline” image ); } else { // Respond with a “saving data” image } }) ); }

Slide 166

Slide 166

@AaronGustafson Use that information else if ( request.headers.get(“Accept”).includes(“image”) ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( // Respond with an “offline” image ); } else { // Respond with a “saving data” image } }) ); }

Slide 167

Slide 167

@AaronGustafson Use that information else if ( request.headers.get(“Accept”).includes(“image”) ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( // Respond with an “offline” image ); } else { // Respond with a “saving data” image } }) ); }

Slide 168

Slide 168

@AaronGustafson Dynamic images? Yes please! const VERSION = “v2”, OFFLINE_PAGE = “offline.html”, SVG_OFFLINE SVG_SLOW = ‘<svg …></svg>’, = ‘<svg …></svg>’;

Slide 169

Slide 169

@AaronGustafson Dynamic images? Yes please! function newSVGResponse( svg ) { return new Response( svg, { headers: { ‘Content-Type’: ‘image/svg+xml’ } }); }

Slide 170

Slide 170

@AaronGustafson An SVG for your troubles else if ( request.headers.get(“Accept”).includes(“image”) ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( // Respond with an “offline” image ); } else { // Respond with a “saving data” image } }) ); }

Slide 171

Slide 171

@AaronGustafson An SVG for your troubles else if ( request.headers.get(“Accept”).includes(“image”) ) { event.respondWith( caches.match( request ).then( cached_result => { // cached first if ( cached_result ) { return cached_result; } // fallback to network if ( ! slow_connection && ! save_data ) { return fetch( request ) .then( response => cacheResponse( response, event ) ) // fail .catch( () => newSVGResponse( SVG_OFFLINE ) ); } else { return newSVGResponse( SVG_SLOW ); } }) ); }

Slide 172

Slide 172

@AaronGustafson Let‘s discuss ๏ What other ways we could use Service Workers to improve the user experience?

Slide 173

Slide 173

@AaronGustafson User Experience Moar enhancements! Capabilities

Slide 174

Slide 174

@AaronGustafson Bonus content! ๏ Rich Installation / Store Pages ๏ Shortcuts ๏ Share Target

Slide 175

Slide 175

@AaronGustafson Bonus content! ๏ Rich Installation / Store Pages ๏ Shortcuts ๏ Share Target

Slide 176

Slide 176

@AaronGustafson Marketing your PWA

Slide 177

Slide 177

@AaronGustafson Name / Short Name

Slide 178

Slide 178

@AaronGustafson Icons

Slide 179

Slide 179

@AaronGustafson Description

Slide 180

Slide 180

@AaronGustafson Description { “name”: “Squoosh”, “icons”: [ … ], “description”: “Make images smaller using best-in-class…”, “screenshots”: [ … ] }

Slide 181

Slide 181

@AaronGustafson Screenshots

Slide 182

Slide 182

@AaronGustafson Screenshots { “name”: “Squoosh”, “icons”: [ … ], “description”: “Make images smaller using best-in-class…”, “screenshots”: [ … ] }

Slide 183

Slide 183

@AaronGustafson Screenshots need labels { “src”: “/i/screenshots/main-ui.png”, “type”: “image/png”, “sizes”: “800x600”, “label”: “The main Squoosh window. Pick an image to get started.” }

Slide 184

Slide 184

@AaronGustafson If screenshots are not universal { “src”: “/i/screenshots/main-ui.png”, “type”: “image/png”, “sizes”: “800x600”, “label”: “The main Squoosh window. Pick an image to get started.”, “platform”: “wide” }

Slide 185

Slide 185

@AaronGustafson Manifest App Information ๏ description ๏ screenshots ๏ categories ๏ iarc_rating_id ๏ Under consideration: ๏ Developer/Publisher ๏ Policies ๏ Banner Graphic ๏ Change Log

Slide 186

Slide 186

@AaronGustafson Bonus content! ๏ Rich Installation / Store Pages ๏ Shortcuts ๏ Share Target

Slide 187

Slide 187

@AaronGustafson Quick access to key tasks { … “shortcuts”: [ … ] }

Slide 188

Slide 188

@AaronGustafson Anatomy of a Shortcut Item { “name”: “New Tweet”, “url”: “/compose/tweet”, “icons”: [{ “src”: “icon-compose.png”, “type”: “image/png”, “sizes”: “192x192” }] }

Slide 189

Slide 189

@AaronGustafson Anatomy of a Shortcut Item { “name”: “New Tweet”, “url”: “/compose/tweet”, “icons”: [{ “src”: “icon-compose.png”, “type”: “image/png”, “sizes”: “192x192” }] }

Slide 190

Slide 190

@AaronGustafson Anatomy of a Shortcut Item { “name”: “New Tweet”, “url”: “/compose/tweet”, “icons”: [{ “src”: “icon-compose.png”, “type”: “image/png”, “sizes”: “192x192” }] }

Slide 191

Slide 191

@AaronGustafson Bonus content! ๏ Rich Installation / Store Pages ๏ Shortcuts ๏ Share Target

Slide 192

Slide 192

@AaronGustafson Easy sharing to your app { … “share_target”: {} }

Slide 193

Slide 193

@AaronGustafson Anatomy of a Share Target “share_target”: { “action”: “linky/poo/”, “method”: “GET”, “enctype”: “application/x-www-form-urlencoded”, “params”: { “title”: “title”, “text”: “body”, “url”: “url” } }

Slide 194

Slide 194

@AaronGustafson Anatomy of a Share Target “share_target”: { “action”: “linky/poo/”, “method”: “GET”, “enctype”: “application/x-www-form-urlencoded”, “params”: { “title”: “title”, “text”: “body”, “url”: “url” } }

Slide 195

Slide 195

@AaronGustafson Anatomy of a Share Target “share_target”: { “action”: “linky/poo/”, “method”: “GET”, “enctype”: “application/x-www-form-urlencoded”, “params”: { “title”: “title”, “text”: “body”, “url”: “url” } }

Slide 196

Slide 196

@AaronGustafson Anatomy of a Share Target “share_target”: { “action”: “linky/poo/”, “method”: “GET”, “enctype”: “application/x-www-form-urlencoded”, “params”: { “title”: “title”, “text”: “body”, “url”: “url” } }

Slide 197

Slide 197

@AaronGustafson You can also accept files “params”: { “text”: “text”, “url”: “url”, “files”: [{ “name”: “externalMedia”, “accept”: [ “image/jpeg”, “image/png”, “image/gif”, “video/quicktime”, “video/mp4” ] }]

Slide 198

Slide 198

PWAs start with a great web experience and then enhance that experience for performance, resilience, installation, and engagement

Slide 199

Slide 199

PWAs start with a great web experience and then enhance that experience for performance, resilience, installation, and engagement

Slide 200

Slide 200

@AaronGustafson Congrats, you made a PWA! HTTPS Web App Manifest Service Worker

Slide 201

Slide 201

Thank you! @AaronGustafson aaron-gustafson.com noti.st/AaronGustafson