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

@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

What exactly is a PWA?

What exactly is a Progressive Web App?

What exactly is a Progressive Web App?

“Progressive Progressive Web App” App is a marketing term

Progressive Web App

Game Gallery Book Progressive Web App Newspaper Art Project Tool

Progressive Web Site

Who’s behind PWAs?

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

Should I believe the hype?

Maybe?

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

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

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

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

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

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

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

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

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

Progressive Web App

Progressive Web App Enhancement

@AaronGustafson User Experience Enhance the experience Capabilities

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

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

@AaronGustafson Let’s talk about HTTPS

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

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

@AaronGustafson Let’s talk about the Manifest

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

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

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

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

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

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

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

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

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

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

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

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

@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

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

How’d it go? Any questions?

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

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

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

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

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

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

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

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

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

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

@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” } ] }

@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” } ] }

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

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

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

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

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

@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

@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

@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

@AaronGustafson User Experience Enhance the experience Capabilities

@AaronGustafson User Experience Enhance the experience Capabilities

How’d it go? Any questions?

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

@AaronGustafson Let’s talk about Service Worker

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

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

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

@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

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

@AaronGustafson User Experience Enhance the experience Capabilities

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

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

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

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

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

@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

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

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

@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” ]); }) ); });

@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” ]); }) ); });

@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” ]); }) ); });

@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” ]); }) ); });

@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” ]); }) ); });

@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” ]); }) ); });

@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” ]); }) ); });

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

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

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

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

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

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

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

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

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

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

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

@AaronGustafson Talk about the network

@AaronGustafson How requests are made Browser Internet

@AaronGustafson Along comes Service Worker Browser Internet

@AaronGustafson Along comes Service Worker Browser Cache Internet

@AaronGustafson Along comes Service Worker Browser ! Cache Internet

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

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

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

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

What if the request fails?

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

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

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

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

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

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

@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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@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.” }) ); }) ); }

@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.” }) ); }) ); }

@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.” }) ); }) ); }

@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.” }) ); }) ); }

@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.” }) ); }) ); }

@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.” }) ); }) ); }

Anyone see an opportunity for refactoring?

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

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

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

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

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

@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.” }) ); }) ); }

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

@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

@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

@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

@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

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

Could we save data?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@AaronGustafson User Experience Moar enhancements! Capabilities

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

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

@AaronGustafson Marketing your PWA

@AaronGustafson Name / Short Name

@AaronGustafson Icons

@AaronGustafson Description

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

@AaronGustafson Screenshots

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

@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.” }

@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” }

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

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

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

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

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

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

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

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

@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” } }

@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” } }

@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” } }

@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” } }

@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” ] }]

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

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

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

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