Getting Started with PWAs [Workshop]

A presentation at PWA Summit in October 2022 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

Debenhams: 40% increase in mobile revenue 20% increase in conversion aka.ms/debenhams-pwa-2018

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

Progressive Web App

Slide 24

Slide 24

Progressive Web App Enhancement

Slide 25

Slide 25

@AaronGustafson User Experience Enhance the experience Capabilities

Slide 26

Slide 26

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

Slide 27

Slide 27

@AaronGustafson Let’s talk about HTTPS

Slide 28

Slide 28

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

Slide 29

Slide 29

@AaronGustafson Let’s talk about the Manifest

Slide 30

Slide 30

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

Slide 31

Slide 31

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

Slide 32

Slide 32

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

Slide 33

Slide 33

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

Slide 34

Slide 34

@AaronGustafson Minimum Viable Manifest { “lang”: “en-US”, “dir”: “auto”, // default “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”, “name”: “Aaron Gustafson”, “start_url”: “/” }

Slide 37

Slide 37

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

Slide 38

Slide 38

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

Slide 39

Slide 39

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

Slide 40

Slide 40

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

Slide 41

Slide 41

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

Slide 42

Slide 42

@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 43

Slide 43

@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 44

Slide 44

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

Slide 45

Slide 45

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

Slide 46

Slide 46

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

Slide 47

Slide 47

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

Slide 48

Slide 48

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

Slide 49

Slide 49

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

Slide 50

Slide 50

@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 51

Slide 51

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

Slide 52

Slide 52

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

Slide 53

Slide 53

@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 54

Slide 54

@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 55

Slide 55

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

Slide 56

Slide 56

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

Slide 57

Slide 57

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

Slide 58

Slide 58

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

Slide 59

Slide 59

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

Slide 60

Slide 60

@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 61

Slide 61

@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 62

Slide 62

@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 63

Slide 63

@AaronGustafson User Experience Enhance the experience Capabilities

Slide 64

Slide 64

@AaronGustafson User Experience Enhance the experience Capabilities

Slide 65

Slide 65

@AaronGustafson Let’s talk about Service Worker

Slide 66

Slide 66

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

Slide 67

Slide 67

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

Slide 68

Slide 68

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

Slide 69

Slide 69

@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 70

Slide 70

@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 71

Slide 71

@AaronGustafson User Experience Enhance the experience Capabilities

Slide 72

Slide 72

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

Slide 73

Slide 73

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

Slide 74

Slide 74

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

Slide 75

Slide 75

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

Slide 76

Slide 76

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

Slide 77

Slide 77

@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 78

Slide 78

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

Slide 79

Slide 79

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

Slide 80

Slide 80

@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 81

Slide 81

@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 82

Slide 82

@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 83

Slide 83

@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 84

Slide 84

@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 85

Slide 85

@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 86

Slide 86

@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 87

Slide 87

@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 88

Slide 88

@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 89

Slide 89

@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 90

Slide 90

@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 91

Slide 91

@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 92

Slide 92

@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 93

Slide 93

@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 94

Slide 94

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

Slide 95

Slide 95

@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 96

Slide 96

@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 97

Slide 97

@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 98

Slide 98

@AaronGustafson Talk about the network

Slide 99

Slide 99

@AaronGustafson How requests are made Browser Internet

Slide 100

Slide 100

@AaronGustafson Along comes Service Worker Browser Internet

Slide 101

Slide 101

@AaronGustafson Along comes Service Worker Browser Cache Internet

Slide 102

Slide 102

@AaronGustafson Along comes Service Worker Browser ! Cache Internet

Slide 103

Slide 103

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

Slide 104

Slide 104

@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 105

Slide 105

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

Slide 106

Slide 106

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

Slide 107

Slide 107

What if the request fails?

Slide 108

Slide 108

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

Slide 109

Slide 109

@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 110

Slide 110

@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 111

Slide 111

@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 112

Slide 112

@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 113

Slide 113

@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 114

Slide 114

@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 115

Slide 115

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

Slide 116

Slide 116

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

Slide 117

Slide 117

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

Slide 118

Slide 118

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

Slide 119

Slide 119

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

Slide 120

Slide 120

@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 121

Slide 121

@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 122

Slide 122

@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 123

Slide 123

@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 124

Slide 124

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

Slide 125

Slide 125

@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 126

Slide 126

@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 127

Slide 127

@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 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( request ).then( cached_result => { if ( cached_result ) { return cached_result; } return caches.match( OFFLINE_PAGE ); }); }) ); }

Slide 129

Slide 129

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

Slide 130

Slide 130

@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 131

Slide 131

@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 132

Slide 132

@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 133

Slide 133

@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 134

Slide 134

@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 135

Slide 135

@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 136

Slide 136

Anyone see an opportunity for refactoring?

Slide 137

Slide 137

@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 138

Slide 138

@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 139

Slide 139

@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 140

Slide 140

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

Slide 141

Slide 141

@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 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 => cacheResponse( response, event ) ) .catch( new Response( “”, { status: 408, statusText: “The server appears to be offline.” }) ); }) ); }

Slide 143

Slide 143

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

Slide 144

Slide 144

@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 145

Slide 145

@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 146

Slide 146

@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 147

Slide 147

@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 148

Slide 148

@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 149

Slide 149

Could we save data?

Slide 150

Slide 150

@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 151

Slide 151

@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 152

Slide 152

@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 153

Slide 153

@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 154

Slide 154

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

Slide 155

Slide 155

@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 156

Slide 156

@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 157

Slide 157

@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 158

Slide 158

@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 159

Slide 159

@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 160

Slide 160

@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 161

Slide 161

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

Slide 162

Slide 162

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

Slide 163

Slide 163

@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 164

Slide 164

@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 165

Slide 165

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

Slide 166

Slide 166

@AaronGustafson User Experience Moar enhancements! Capabilities

Slide 167

Slide 167

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

Slide 168

Slide 168

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

Slide 169

Slide 169

@AaronGustafson Marketing your PWA

Slide 170

Slide 170

@AaronGustafson Name / Short Name

Slide 171

Slide 171

@AaronGustafson Icons

Slide 172

Slide 172

@AaronGustafson Description

Slide 173

Slide 173

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

Slide 174

Slide 174

@AaronGustafson Screenshots

Slide 175

Slide 175

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

Slide 176

Slide 176

@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 177

Slide 177

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

Slide 178

Slide 178

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

Slide 179

Slide 179

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

Slide 180

Slide 180

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

Slide 181

Slide 181

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

Slide 182

Slide 182

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

Slide 183

Slide 183

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

Slide 184

Slide 184

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

Slide 185

Slide 185

@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 186

Slide 186

@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 187

Slide 187

@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 188

Slide 188

@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 189

Slide 189

@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 190

Slide 190

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

Slide 191

Slide 191

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

Slide 192

Slide 192

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