SlideShare a Scribd company logo
Getting started with
Aaron Gustafson
@AaronGustafson
noti.st/AaronGustafson
@AaronGustafson
Follow along
āš Slides: https://aka.ms/btconf-pwa-slides
āš Work files: https://aka.ms/btconf-pwa-code
āš Final Demo: https://aka.ms/btconf-pwa-live
What exactly is a
PWA?
Progressive Web App?
What exactly is a
What exactly is a
Progressive Web App?
“Progressive Web App”
is a marketing term
Progressive Web App
Progressive Web App
Progressive Web App
Game
Gallery
Book
Newspaper
Art Project
Tool
Progressive Web App
Game
Gallery
Book
Newspaper
Art Project
Tool
Progressive Web Site
Who’s behind PWAs?
@AaronGustafson
What’s a PWA, technically?
HTTPS
@AaronGustafson
What’s a PWA, technically?
HTTPS Web App
Manifest
@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
aka.ms/google-io-2018
Tinder:
Core experience
with 90% less code
aka.ms/tinder-pwa-2017
Trivago:
97% increase in
click-outs to
hotel offers
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
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 AppProgressive
Enhancement
Progressive
/prəˈɡresiv/
happening or developing
gradually or in stages;
proceeding step by step
@AaronGustafson
Enhance the experience
Capabilities
UserExperience
@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",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Minimum Viable Manifest
{
"lang": "en-US",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Minimum Viable Manifest
{
"lang": "en-US",
"dir": "ltr",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Minimum Viable Manifest
{
"lang": "en-US",
"dir": "auto", // default
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Minimum Viable Manifest
{
"lang": "en-US",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Minimum Viable Manifest
{
"lang": "en-US",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Minimum Viable Manifest
{
"lang": "en-US",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Minimum Viable Manifest
{
"lang": "en-US",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@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">
@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,
2. name,
3. short_name, and
4. description.
@AaronGustafson
Yours should look like this
{
"lang": "en-US",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Let’s prep for installâ€Ļ
{
"lang": "en-US",
"name": "Aaron Gustafson",
"short_name": "AaronG",
"description": "The online home and work of Aaron Gustafson."
}
@AaronGustafson
Where do we begin?
{
â€Ļ
"start_url": "/"
}
@AaronGustafson
Where does this apply?
{
â€Ļ
"start_url": "/",
"scope": "/" // defaults to the start_url value
}
@AaronGustafson
What should it look like?
{
â€Ļ
"start_url": "/",
"display": "minimal-ui"
}
@AaronGustafson
Display Modes
"display": "browser" "display": "standalone" "display": "minimal-ui""display": "fullscreen"
@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": "badge"
}
@AaronGustafson
Anatomy of an ImageResource
{
"src": "/i/og-logo.png",
"type": "image/png",
"sizes": "800x600",
"purpose": "maskable"
}
@AaronGustafson
Let‘s improve our manifest!
āš Add the following Manifest members:
1. start_url,
2. display,
3. theme_color,
4. background_color, and
5. icons
Suggested sizes: 48x48, 72x72, 96x96, 144x144,
192x192, and 512x512
@AaronGustafson
Enhance the experience
Capabilities
UserExperience
@AaronGustafson
Enhance the experience
Capabilities
UserExperience
How’d it go?
Any questions?
Let’s take a break.
@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" );
}
@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
Enhance the experience
Capabilities
UserExperience
@AaronGustafson
The Service Worker Lifecycle
Browser
Install Activation Ready
aka.ms/pwa-lifecycle
@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 Ready
aka.ms/pwa-lifecycle
@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
We need to clean up
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
We need to clean up
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
We need to clean up
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
We need to clean up
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
We need to clean up
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
We need to clean up
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 new 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
Take over 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?
Let’s get
some lunch
@AaronGustafson
Back to Service Worker
@AaronGustafson
How requests are made
Browser
Internet
@AaronGustafson
Along comes Service Worker
Browser
Internet
@AaronGustafson
Along comes Service Worker
Browser
Internet
Cache
@AaronGustafson
Along comes Service Worker
Browser
Internet
Cache
👎đŸģ
@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?
Let’s take
another break
Could we save data?
@AaronGustafson
Look at the connection
var 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
var 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
var 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
var 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 â€Ļ></svg>',
SVG_SLOW = '<svg â€Ļ></svg>';
@AaronGustafson
Dynamic images? Yes please!
function newImageResponse( 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(
() => newImageResponse( SVG_OFFLINE )
);
} else {
return newImageResponse( SVG_SLOW );
}
})
);
}
@AaronGustafson
Let‘s discuss
āš What other ways we could use Service Workers to
improve the user experience?
@AaronGustafson
Moar enhancements!
Capabilities
UserExperience
@AaronGustafson
Bonus content!
āš Share Target
āš Shortcuts (coming soon)
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

More Related Content

What's hot

Elasticsearch Query DSL - Not just for wizards...
Elasticsearch Query DSL - Not just for wizards...Elasticsearch Query DSL - Not just for wizards...
Elasticsearch Query DSL - Not just for wizards...
clintongormley
 
Scaling real-time search and analytics with Elasticsearch
Scaling real-time search and analytics with ElasticsearchScaling real-time search and analytics with Elasticsearch
Scaling real-time search and analytics with Elasticsearch
clintongormley
 
London seo master - feb 2020
London seo master - feb 2020London seo master - feb 2020
London seo master - feb 2020
Matt Williamson
 
다ė–‘í•œ ė—…ëŦ´ė— ė í•Ší•œ AWSė˜ ėŠ¤í† ëĻŦė§€ ė„œëš„ėŠ¤ ė•Œė•„ëŗ´ę¸° – 김ėƒí˜„, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Ser...
다ė–‘í•œ ė—…ëŦ´ė— ė í•Ší•œ AWSė˜ ėŠ¤í† ëĻŦė§€ ė„œëš„ėŠ¤ ė•Œė•„ëŗ´ę¸° – 김ėƒí˜„, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Ser...다ė–‘í•œ ė—…ëŦ´ė— ė í•Ší•œ AWSė˜ ėŠ¤í† ëĻŦė§€ ė„œëš„ėŠ¤ ė•Œė•„ëŗ´ę¸° – 김ėƒí˜„, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Ser...
다ė–‘í•œ ė—…ëŦ´ė— ė í•Ší•œ AWSė˜ ėŠ¤í† ëĻŦė§€ ė„œëš„ėŠ¤ ė•Œė•„ëŗ´ę¸° – 김ėƒí˜„, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Ser...
Amazon Web Services Korea
 
Clinton Gormley – Elasticsearch Query DSL – Not just for wizardsâ€Ļ- NoSQL matt...
Clinton Gormley – Elasticsearch Query DSL – Not just for wizardsâ€Ļ- NoSQL matt...Clinton Gormley – Elasticsearch Query DSL – Not just for wizardsâ€Ļ- NoSQL matt...
Clinton Gormley – Elasticsearch Query DSL – Not just for wizardsâ€Ļ- NoSQL matt...
NoSQLmatters
 
Redefining technical SEO & how we should be thinking about it as an industry ...
Redefining technical SEO & how we should be thinking about it as an industry ...Redefining technical SEO & how we should be thinking about it as an industry ...
Redefining technical SEO & how we should be thinking about it as an industry ...
WeLoveSEO
 
Google search secrets
Google search secretsGoogle search secrets
Google search secrets
Saurabh Gupta
 
Alexis max-Creating a bot experience as good as your user experience - Alexis...
Alexis max-Creating a bot experience as good as your user experience - Alexis...Alexis max-Creating a bot experience as good as your user experience - Alexis...
Alexis max-Creating a bot experience as good as your user experience - Alexis...
WeLoveSEO
 
Enfuse_2016_Internet_of Things_Rajewski
Enfuse_2016_Internet_of Things_RajewskiEnfuse_2016_Internet_of Things_Rajewski
Enfuse_2016_Internet_of Things_RajewskiMary Braden Murphy
 
AWS re:Invent 2020 IoT Update - 20201223
AWS re:Invent 2020 IoT Update - 20201223AWS re:Invent 2020 IoT Update - 20201223
AWS re:Invent 2020 IoT Update - 20201223
Amazon Web Services Japan
 
Searching 101
Searching 101Searching 101
Searching 101
Pilgrim Library
 
ReadingSEO - Technical SEO at Scale
ReadingSEO - Technical SEO at ScaleReadingSEO - Technical SEO at Scale
ReadingSEO - Technical SEO at Scale
Hayden Roche
 
AWS ė„œëš„ėŠ¤ëĄœ ė›š ė• í”ŒëĻŦėŧ€ė´ė…˜ 만들기 – 김ėŖŧė˜, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series
AWS ė„œëš„ėŠ¤ëĄœ ė›š ė• í”ŒëĻŦėŧ€ė´ė…˜ 만들기 – 김ėŖŧė˜, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series AWS ė„œëš„ėŠ¤ëĄœ ė›š ė• í”ŒëĻŦėŧ€ė´ė…˜ 만들기 – 김ėŖŧė˜, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series
AWS ė„œëš„ėŠ¤ëĄœ ė›š ė• í”ŒëĻŦėŧ€ė´ė…˜ 만들기 – 김ėŖŧė˜, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series
Amazon Web Services Korea
 
AWS Migration - General
AWS Migration - GeneralAWS Migration - General
AWS Migration - General
Kenji Morooka
 
AWS 클ëŧėš°ë“œ 환ę˛Ŋė—ė„œė˜ VDI 환ę˛Ŋ 및 ėŊœ ė„ŧ터 ęĩŦėļ•í•˜ę¸° – ėĄ°ėƒë§Œ, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online...
 AWS 클ëŧėš°ë“œ 환ę˛Ŋė—ė„œė˜ VDI 환ę˛Ŋ 및 ėŊœ ė„ŧ터 ęĩŦėļ•í•˜ę¸° – ėĄ°ėƒë§Œ, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online... AWS 클ëŧėš°ë“œ 환ę˛Ŋė—ė„œė˜ VDI 환ę˛Ŋ 및 ėŊœ ė„ŧ터 ęĩŦėļ•í•˜ę¸° – ėĄ°ėƒë§Œ, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online...
AWS 클ëŧėš°ë“œ 환ę˛Ŋė—ė„œė˜ VDI 환ę˛Ŋ 및 ėŊœ ė„ŧ터 ęĩŦėļ•í•˜ę¸° – ėĄ°ėƒë§Œ, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online...
Amazon Web Services Korea
 
ėŠ¤íƒ€íŠ¸ė—…ė—ė„œ 데ė´í„° ëļ„ė„ ė‹œėž‘í•˜ę¸° – 박ė§„ėš°, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series
ėŠ¤íƒ€íŠ¸ė—…ė—ė„œ 데ė´í„° ëļ„ė„ ė‹œėž‘í•˜ę¸° – 박ė§„ėš°, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series ėŠ¤íƒ€íŠ¸ė—…ė—ė„œ 데ė´í„° ëļ„ė„ ė‹œėž‘í•˜ę¸° – 박ė§„ėš°, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series
ėŠ¤íƒ€íŠ¸ė—…ė—ė„œ 데ė´í„° ëļ„ė„ ė‹œėž‘í•˜ę¸° – 박ė§„ėš°, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series
Amazon Web Services Korea
 
Love Can't Wait! Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]
Love Can't Wait!  Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]Love Can't Wait!  Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]
Love Can't Wait! Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]
New Relic
 
ã‚˛ãƒŧム開į™ēでæ´ģį”¨ã™ã‚‹AWSぎ抟æĸ°å­Ļįŋ’ã‚ĩãƒŧビ゚ぎį´šäģ‹
ã‚˛ãƒŧム開į™ēでæ´ģį”¨ã™ã‚‹AWSぎ抟æĸ°å­Ļįŋ’ã‚ĩãƒŧビ゚ぎį´šäģ‹ã‚˛ãƒŧム開į™ēでæ´ģį”¨ã™ã‚‹AWSぎ抟æĸ°å­Ļįŋ’ã‚ĩãƒŧビ゚ぎį´šäģ‹
ã‚˛ãƒŧム開į™ēでæ´ģį”¨ã™ã‚‹AWSぎ抟æĸ°å­Ļįŋ’ã‚ĩãƒŧビ゚ぎį´šäģ‹
Amazon Web Services Japan
 

What's hot (19)

Elasticsearch Query DSL - Not just for wizards...
Elasticsearch Query DSL - Not just for wizards...Elasticsearch Query DSL - Not just for wizards...
Elasticsearch Query DSL - Not just for wizards...
 
Scaling real-time search and analytics with Elasticsearch
Scaling real-time search and analytics with ElasticsearchScaling real-time search and analytics with Elasticsearch
Scaling real-time search and analytics with Elasticsearch
 
London seo master - feb 2020
London seo master - feb 2020London seo master - feb 2020
London seo master - feb 2020
 
다ė–‘í•œ ė—…ëŦ´ė— ė í•Ší•œ AWSė˜ ėŠ¤í† ëĻŦė§€ ė„œëš„ėŠ¤ ė•Œė•„ëŗ´ę¸° – 김ėƒí˜„, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Ser...
다ė–‘í•œ ė—…ëŦ´ė— ė í•Ší•œ AWSė˜ ėŠ¤í† ëĻŦė§€ ė„œëš„ėŠ¤ ė•Œė•„ëŗ´ę¸° – 김ėƒí˜„, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Ser...다ė–‘í•œ ė—…ëŦ´ė— ė í•Ší•œ AWSė˜ ėŠ¤í† ëĻŦė§€ ė„œëš„ėŠ¤ ė•Œė•„ëŗ´ę¸° – 김ėƒí˜„, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Ser...
다ė–‘í•œ ė—…ëŦ´ė— ė í•Ší•œ AWSė˜ ėŠ¤í† ëĻŦė§€ ė„œëš„ėŠ¤ ė•Œė•„ëŗ´ę¸° – 김ėƒí˜„, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Ser...
 
Clinton Gormley – Elasticsearch Query DSL – Not just for wizardsâ€Ļ- NoSQL matt...
Clinton Gormley – Elasticsearch Query DSL – Not just for wizardsâ€Ļ- NoSQL matt...Clinton Gormley – Elasticsearch Query DSL – Not just for wizardsâ€Ļ- NoSQL matt...
Clinton Gormley – Elasticsearch Query DSL – Not just for wizardsâ€Ļ- NoSQL matt...
 
Redefining technical SEO & how we should be thinking about it as an industry ...
Redefining technical SEO & how we should be thinking about it as an industry ...Redefining technical SEO & how we should be thinking about it as an industry ...
Redefining technical SEO & how we should be thinking about it as an industry ...
 
Google search secrets
Google search secretsGoogle search secrets
Google search secrets
 
Alexis max-Creating a bot experience as good as your user experience - Alexis...
Alexis max-Creating a bot experience as good as your user experience - Alexis...Alexis max-Creating a bot experience as good as your user experience - Alexis...
Alexis max-Creating a bot experience as good as your user experience - Alexis...
 
Enfuse_2016_Internet_of Things_Rajewski
Enfuse_2016_Internet_of Things_RajewskiEnfuse_2016_Internet_of Things_Rajewski
Enfuse_2016_Internet_of Things_Rajewski
 
AWS re:Invent 2020 IoT Update - 20201223
AWS re:Invent 2020 IoT Update - 20201223AWS re:Invent 2020 IoT Update - 20201223
AWS re:Invent 2020 IoT Update - 20201223
 
Owasp austin
Owasp austinOwasp austin
Owasp austin
 
Searching 101
Searching 101Searching 101
Searching 101
 
ReadingSEO - Technical SEO at Scale
ReadingSEO - Technical SEO at ScaleReadingSEO - Technical SEO at Scale
ReadingSEO - Technical SEO at Scale
 
AWS ė„œëš„ėŠ¤ëĄœ ė›š ė• í”ŒëĻŦėŧ€ė´ė…˜ 만들기 – 김ėŖŧė˜, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series
AWS ė„œëš„ėŠ¤ëĄœ ė›š ė• í”ŒëĻŦėŧ€ė´ė…˜ 만들기 – 김ėŖŧė˜, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series AWS ė„œëš„ėŠ¤ëĄœ ė›š ė• í”ŒëĻŦėŧ€ė´ė…˜ 만들기 – 김ėŖŧė˜, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series
AWS ė„œëš„ėŠ¤ëĄœ ė›š ė• í”ŒëĻŦėŧ€ė´ė…˜ 만들기 – 김ėŖŧė˜, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series
 
AWS Migration - General
AWS Migration - GeneralAWS Migration - General
AWS Migration - General
 
AWS 클ëŧėš°ë“œ 환ę˛Ŋė—ė„œė˜ VDI 환ę˛Ŋ 및 ėŊœ ė„ŧ터 ęĩŦėļ•í•˜ę¸° – ėĄ°ėƒë§Œ, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online...
 AWS 클ëŧėš°ë“œ 환ę˛Ŋė—ė„œė˜ VDI 환ę˛Ŋ 및 ėŊœ ė„ŧ터 ęĩŦėļ•í•˜ę¸° – ėĄ°ėƒë§Œ, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online... AWS 클ëŧėš°ë“œ 환ę˛Ŋė—ė„œė˜ VDI 환ę˛Ŋ 및 ėŊœ ė„ŧ터 ęĩŦėļ•í•˜ę¸° – ėĄ°ėƒë§Œ, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online...
AWS 클ëŧėš°ë“œ 환ę˛Ŋė—ė„œė˜ VDI 환ę˛Ŋ 및 ėŊœ ė„ŧ터 ęĩŦėļ•í•˜ę¸° – ėĄ°ėƒë§Œ, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online...
 
ėŠ¤íƒ€íŠ¸ė—…ė—ė„œ 데ė´í„° ëļ„ė„ ė‹œėž‘í•˜ę¸° – 박ė§„ėš°, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series
ėŠ¤íƒ€íŠ¸ė—…ė—ė„œ 데ė´í„° ëļ„ė„ ė‹œėž‘í•˜ę¸° – 박ė§„ėš°, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series ėŠ¤íƒ€íŠ¸ė—…ė—ė„œ 데ė´í„° ëļ„ė„ ė‹œėž‘í•˜ę¸° – 박ė§„ėš°, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series
ėŠ¤íƒ€íŠ¸ė—…ė—ė„œ 데ė´í„° ëļ„ė„ ė‹œėž‘í•˜ę¸° – 박ė§„ėš°, AWS ė†”ëŖ¨ė…˜ėĻˆ ė•„키텍트:: AWS Builders Online Series
 
Love Can't Wait! Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]
Love Can't Wait!  Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]Love Can't Wait!  Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]
Love Can't Wait! Optimizing PageLoad Time of SPAs at Zoosk [FutureStack16]
 
ã‚˛ãƒŧム開į™ēでæ´ģį”¨ã™ã‚‹AWSぎ抟æĸ°å­Ļįŋ’ã‚ĩãƒŧビ゚ぎį´šäģ‹
ã‚˛ãƒŧム開į™ēでæ´ģį”¨ã™ã‚‹AWSぎ抟æĸ°å­Ļįŋ’ã‚ĩãƒŧビ゚ぎį´šäģ‹ã‚˛ãƒŧム開į™ēでæ´ģį”¨ã™ã‚‹AWSぎ抟æĸ°å­Ļįŋ’ã‚ĩãƒŧビ゚ぎį´šäģ‹
ã‚˛ãƒŧム開į™ēでæ´ģį”¨ã™ã‚‹AWSぎ抟æĸ°å­Ļįŋ’ã‚ĩãƒŧビ゚ぎį´šäģ‹
 

Similar to Getting Started with Progressive Web Apps [Beyond Tellerrand 2019]

Media in the Age of PWAs [ImageCon 2019]
Media in the Age of PWAs [ImageCon 2019]Media in the Age of PWAs [ImageCon 2019]
Media in the Age of PWAs [ImageCon 2019]
Aaron Gustafson
 
Effectively Testing Services on Rails - Railsconf 2014
Effectively Testing Services on Rails - Railsconf 2014Effectively Testing Services on Rails - Railsconf 2014
Effectively Testing Services on Rails - Railsconf 2014
neal_kemp
 
Designing the Conversation [Beyond Tellerrand 2019]
Designing the Conversation [Beyond Tellerrand 2019]Designing the Conversation [Beyond Tellerrand 2019]
Designing the Conversation [Beyond Tellerrand 2019]
Aaron Gustafson
 
ã‚ŧロから始める
ã‚ģマãƒŗテã‚ŖックSEO
&
構造化デãƒŧã‚ŋ at CSS Nite LP36
ã‚ŧロから始める
ã‚ģマãƒŗテã‚ŖックSEO
&
構造化デãƒŧã‚ŋ at CSS Nite LP36ã‚ŧロから始める
ã‚ģマãƒŗテã‚ŖックSEO
&
構造化デãƒŧã‚ŋ at CSS Nite LP36
ã‚ŧロから始める
ã‚ģマãƒŗテã‚ŖックSEO
&
構造化デãƒŧã‚ŋ at CSS Nite LP36
Kenichi Suzuki
 
Interactive Visualization With Bokeh (SF Python Meetup)
Interactive Visualization With Bokeh (SF Python Meetup)Interactive Visualization With Bokeh (SF Python Meetup)
Interactive Visualization With Bokeh (SF Python Meetup)
Peter Wang
 
Designing the Conversation [Concatenate 2018]
Designing the Conversation [Concatenate 2018]Designing the Conversation [Concatenate 2018]
Designing the Conversation [Concatenate 2018]
Aaron Gustafson
 
Advanced Structured Data: Beyond Rich Snippets
Advanced Structured Data: Beyond Rich SnippetsAdvanced Structured Data: Beyond Rich Snippets
Advanced Structured Data: Beyond Rich SnippetsJustin Briggs
 
Conversational Semantics for the Web [CascadiaJS 2018]
Conversational Semantics for the Web [CascadiaJS 2018]Conversational Semantics for the Web [CascadiaJS 2018]
Conversational Semantics for the Web [CascadiaJS 2018]
Aaron Gustafson
 
Modelagem de Dados Semiestruturados com ISIS-DM
Modelagem de Dados Semiestruturados com ISIS-DMModelagem de Dados Semiestruturados com ISIS-DM
Modelagem de Dados Semiestruturados com ISIS-DM
Gustavo Fonseca
 
SMX West 2020 - Leveraging Structured Data for Maximum Effect
SMX West  2020 - Leveraging Structured Data for Maximum EffectSMX West  2020 - Leveraging Structured Data for Maximum Effect
SMX West 2020 - Leveraging Structured Data for Maximum Effect
Abby Hamilton
 
Sphinx 1.1 i18n 抟čƒŊį´šäģ‹
Sphinx 1.1 i18n 抟čƒŊį´šäģ‹Sphinx 1.1 i18n 抟čƒŊį´šäģ‹
Sphinx 1.1 i18n 抟čƒŊį´šäģ‹
Ian Lewis
 
Smart mobile storytelling christy robinson - denver news train - april 11-1...
Smart mobile storytelling   christy robinson - denver news train - april 11-1...Smart mobile storytelling   christy robinson - denver news train - april 11-1...
Smart mobile storytelling christy robinson - denver news train - april 11-1...
News Leaders Association's NewsTrain
 
Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...
Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...
Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...
Mario Heiderich
 
Embracing Capybara
Embracing CapybaraEmbracing Capybara
Embracing Capybara
Tim Moore
 
Embedding Linked Data Invisibly into Web Pages: Strategies and Workflows for ...
Embedding Linked Data Invisibly into Web Pages: Strategies and Workflows for ...Embedding Linked Data Invisibly into Web Pages: Strategies and Workflows for ...
Embedding Linked Data Invisibly into Web Pages: Strategies and Workflows for ...
National Information Standards Organization (NISO)
 
AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...
AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...
AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...
Amazon Web Services
 
GraphQL & Relay īŧ 串čĩˇå‰åžŒįĢ¯ä¸–į•Œįš„抋樑
GraphQL & Relay īŧ 串čĩˇå‰åžŒįĢ¯ä¸–į•Œįš„抋樑GraphQL & Relay īŧ 串čĩˇå‰åžŒįĢ¯ä¸–į•Œįš„抋樑
GraphQL & Relay īŧ 串čĩˇå‰åžŒįĢ¯ä¸–į•Œįš„抋樑
Pokai Chang
 
Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs - Front in Bahia...
Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs  - Front in Bahia...Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs  - Front in Bahia...
Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs - Front in Bahia...
Ícaro Medeiros
 
Reification
ReificationReification
Reification
Shunsaku Kudo
 
Activity streams Lightning Talk, DjangoCon 2011, Day3
Activity streams Lightning Talk, DjangoCon 2011, Day3Activity streams Lightning Talk, DjangoCon 2011, Day3
Activity streams Lightning Talk, DjangoCon 2011, Day3Steve Ivy
 

Similar to Getting Started with Progressive Web Apps [Beyond Tellerrand 2019] (20)

Media in the Age of PWAs [ImageCon 2019]
Media in the Age of PWAs [ImageCon 2019]Media in the Age of PWAs [ImageCon 2019]
Media in the Age of PWAs [ImageCon 2019]
 
Effectively Testing Services on Rails - Railsconf 2014
Effectively Testing Services on Rails - Railsconf 2014Effectively Testing Services on Rails - Railsconf 2014
Effectively Testing Services on Rails - Railsconf 2014
 
Designing the Conversation [Beyond Tellerrand 2019]
Designing the Conversation [Beyond Tellerrand 2019]Designing the Conversation [Beyond Tellerrand 2019]
Designing the Conversation [Beyond Tellerrand 2019]
 
ã‚ŧロから始める
ã‚ģマãƒŗテã‚ŖックSEO
&
構造化デãƒŧã‚ŋ at CSS Nite LP36
ã‚ŧロから始める
ã‚ģマãƒŗテã‚ŖックSEO
&
構造化デãƒŧã‚ŋ at CSS Nite LP36ã‚ŧロから始める
ã‚ģマãƒŗテã‚ŖックSEO
&
構造化デãƒŧã‚ŋ at CSS Nite LP36
ã‚ŧロから始める
ã‚ģマãƒŗテã‚ŖックSEO
&
構造化デãƒŧã‚ŋ at CSS Nite LP36
 
Interactive Visualization With Bokeh (SF Python Meetup)
Interactive Visualization With Bokeh (SF Python Meetup)Interactive Visualization With Bokeh (SF Python Meetup)
Interactive Visualization With Bokeh (SF Python Meetup)
 
Designing the Conversation [Concatenate 2018]
Designing the Conversation [Concatenate 2018]Designing the Conversation [Concatenate 2018]
Designing the Conversation [Concatenate 2018]
 
Advanced Structured Data: Beyond Rich Snippets
Advanced Structured Data: Beyond Rich SnippetsAdvanced Structured Data: Beyond Rich Snippets
Advanced Structured Data: Beyond Rich Snippets
 
Conversational Semantics for the Web [CascadiaJS 2018]
Conversational Semantics for the Web [CascadiaJS 2018]Conversational Semantics for the Web [CascadiaJS 2018]
Conversational Semantics for the Web [CascadiaJS 2018]
 
Modelagem de Dados Semiestruturados com ISIS-DM
Modelagem de Dados Semiestruturados com ISIS-DMModelagem de Dados Semiestruturados com ISIS-DM
Modelagem de Dados Semiestruturados com ISIS-DM
 
SMX West 2020 - Leveraging Structured Data for Maximum Effect
SMX West  2020 - Leveraging Structured Data for Maximum EffectSMX West  2020 - Leveraging Structured Data for Maximum Effect
SMX West 2020 - Leveraging Structured Data for Maximum Effect
 
Sphinx 1.1 i18n 抟čƒŊį´šäģ‹
Sphinx 1.1 i18n 抟čƒŊį´šäģ‹Sphinx 1.1 i18n 抟čƒŊį´šäģ‹
Sphinx 1.1 i18n 抟čƒŊį´šäģ‹
 
Smart mobile storytelling christy robinson - denver news train - april 11-1...
Smart mobile storytelling   christy robinson - denver news train - april 11-1...Smart mobile storytelling   christy robinson - denver news train - april 11-1...
Smart mobile storytelling christy robinson - denver news train - april 11-1...
 
Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...
Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...
Copy & Pest - A case-study on the clipboard, blind trust and invisible cross-...
 
Embracing Capybara
Embracing CapybaraEmbracing Capybara
Embracing Capybara
 
Embedding Linked Data Invisibly into Web Pages: Strategies and Workflows for ...
Embedding Linked Data Invisibly into Web Pages: Strategies and Workflows for ...Embedding Linked Data Invisibly into Web Pages: Strategies and Workflows for ...
Embedding Linked Data Invisibly into Web Pages: Strategies and Workflows for ...
 
AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...
AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...
AWS re:Invent 2016: State of the Union: Amazon Alexa and Recent Advances in C...
 
GraphQL & Relay īŧ 串čĩˇå‰åžŒįĢ¯ä¸–į•Œįš„抋樑
GraphQL & Relay īŧ 串čĩˇå‰åžŒįĢ¯ä¸–į•Œįš„抋樑GraphQL & Relay īŧ 串čĩˇå‰åžŒįĢ¯ä¸–į•Œįš„抋樑
GraphQL & Relay īŧ 串čĩˇå‰åžŒįĢ¯ä¸–į•Œįš„抋樑
 
Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs - Front in Bahia...
Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs  - Front in Bahia...Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs  - Front in Bahia...
Linked Data in Use: Schema.org, JSON-LD and hypermedia APIs - Front in Bahia...
 
Reification
ReificationReification
Reification
 
Activity streams Lightning Talk, DjangoCon 2011, Day3
Activity streams Lightning Talk, DjangoCon 2011, Day3Activity streams Lightning Talk, DjangoCon 2011, Day3
Activity streams Lightning Talk, DjangoCon 2011, Day3
 

More from Aaron Gustafson

Delivering Critical Information and Services [JavaScript & Friends 2021]
Delivering Critical Information and Services [JavaScript & Friends 2021]Delivering Critical Information and Services [JavaScript & Friends 2021]
Delivering Critical Information and Services [JavaScript & Friends 2021]
Aaron Gustafson
 
Adapting to Reality [Guest Lecture, March 2021]
Adapting to Reality [Guest Lecture, March 2021]Adapting to Reality [Guest Lecture, March 2021]
Adapting to Reality [Guest Lecture, March 2021]
Aaron Gustafson
 
Progressive Web Apps: Where Do I Begin?
Progressive Web Apps: Where Do I Begin?Progressive Web Apps: Where Do I Begin?
Progressive Web Apps: Where Do I Begin?
Aaron Gustafson
 
Adapting to Reality [Starbucks Lunch & Learn]
Adapting to Reality [Starbucks Lunch & Learn]Adapting to Reality [Starbucks Lunch & Learn]
Adapting to Reality [Starbucks Lunch & Learn]
Aaron Gustafson
 
Better Performance === Greater Accessibility [Inclusive Design 24 2018]
Better Performance === Greater Accessibility [Inclusive Design 24 2018]Better Performance === Greater Accessibility [Inclusive Design 24 2018]
Better Performance === Greater Accessibility [Inclusive Design 24 2018]
Aaron Gustafson
 
PWA: Where Do I Begin? [Microsoft Ignite 2018]
PWA: Where Do I Begin? [Microsoft Ignite 2018]PWA: Where Do I Begin? [Microsoft Ignite 2018]
PWA: Where Do I Begin? [Microsoft Ignite 2018]
Aaron Gustafson
 
Designing the Conversation [Accessibility DC 2018]
Designing the Conversation [Accessibility DC 2018]Designing the Conversation [Accessibility DC 2018]
Designing the Conversation [Accessibility DC 2018]
Aaron Gustafson
 
Performance as User Experience [AEADC 2018]
Performance as User Experience [AEADC 2018]Performance as User Experience [AEADC 2018]
Performance as User Experience [AEADC 2018]
Aaron Gustafson
 
The Web Should Just Work for Everyone
The Web Should Just Work for EveryoneThe Web Should Just Work for Everyone
The Web Should Just Work for Everyone
Aaron Gustafson
 
Performance as User Experience [AEA SEA 2018]
Performance as User Experience [AEA SEA 2018]Performance as User Experience [AEA SEA 2018]
Performance as User Experience [AEA SEA 2018]
Aaron Gustafson
 
Performance as User Experience [An Event Apart Denver 2017]
Performance as User Experience [An Event Apart Denver 2017]Performance as User Experience [An Event Apart Denver 2017]
Performance as User Experience [An Event Apart Denver 2017]
Aaron Gustafson
 
Advanced Design Methods 1, Day 2
Advanced Design Methods 1, Day 2Advanced Design Methods 1, Day 2
Advanced Design Methods 1, Day 2
Aaron Gustafson
 
Advanced Design Methods 1, Day 1
Advanced Design Methods 1, Day 1Advanced Design Methods 1, Day 1
Advanced Design Methods 1, Day 1
Aaron Gustafson
 
Designing the Conversation [Paris Web 2017]
Designing the Conversation [Paris Web 2017]Designing the Conversation [Paris Web 2017]
Designing the Conversation [Paris Web 2017]
Aaron Gustafson
 
Exploring Adaptive Interfaces [Generate 2017]
Exploring Adaptive Interfaces [Generate 2017]Exploring Adaptive Interfaces [Generate 2017]
Exploring Adaptive Interfaces [Generate 2017]
Aaron Gustafson
 
Progressive Web Apps and the Windows Ecosystem [Build 2017]
Progressive Web Apps and the Windows Ecosystem [Build 2017]Progressive Web Apps and the Windows Ecosystem [Build 2017]
Progressive Web Apps and the Windows Ecosystem [Build 2017]
Aaron Gustafson
 
Writing for Engagement [TechReady 22]
Writing for Engagement [TechReady 22]Writing for Engagement [TechReady 22]
Writing for Engagement [TechReady 22]
Aaron Gustafson
 
Designing the Conversation [SmashingConf 2016]
Designing the Conversation [SmashingConf 2016]Designing the Conversation [SmashingConf 2016]
Designing the Conversation [SmashingConf 2016]
Aaron Gustafson
 
The Features of Highly Effective Forms [SmashingConf NYC 2016]
The Features of Highly Effective Forms [SmashingConf NYC 2016]The Features of Highly Effective Forms [SmashingConf NYC 2016]
The Features of Highly Effective Forms [SmashingConf NYC 2016]
Aaron Gustafson
 
Designing the Conversation [SpeechTek 2016]
Designing the Conversation [SpeechTek 2016]Designing the Conversation [SpeechTek 2016]
Designing the Conversation [SpeechTek 2016]
Aaron Gustafson
 

More from Aaron Gustafson (20)

Delivering Critical Information and Services [JavaScript & Friends 2021]
Delivering Critical Information and Services [JavaScript & Friends 2021]Delivering Critical Information and Services [JavaScript & Friends 2021]
Delivering Critical Information and Services [JavaScript & Friends 2021]
 
Adapting to Reality [Guest Lecture, March 2021]
Adapting to Reality [Guest Lecture, March 2021]Adapting to Reality [Guest Lecture, March 2021]
Adapting to Reality [Guest Lecture, March 2021]
 
Progressive Web Apps: Where Do I Begin?
Progressive Web Apps: Where Do I Begin?Progressive Web Apps: Where Do I Begin?
Progressive Web Apps: Where Do I Begin?
 
Adapting to Reality [Starbucks Lunch & Learn]
Adapting to Reality [Starbucks Lunch & Learn]Adapting to Reality [Starbucks Lunch & Learn]
Adapting to Reality [Starbucks Lunch & Learn]
 
Better Performance === Greater Accessibility [Inclusive Design 24 2018]
Better Performance === Greater Accessibility [Inclusive Design 24 2018]Better Performance === Greater Accessibility [Inclusive Design 24 2018]
Better Performance === Greater Accessibility [Inclusive Design 24 2018]
 
PWA: Where Do I Begin? [Microsoft Ignite 2018]
PWA: Where Do I Begin? [Microsoft Ignite 2018]PWA: Where Do I Begin? [Microsoft Ignite 2018]
PWA: Where Do I Begin? [Microsoft Ignite 2018]
 
Designing the Conversation [Accessibility DC 2018]
Designing the Conversation [Accessibility DC 2018]Designing the Conversation [Accessibility DC 2018]
Designing the Conversation [Accessibility DC 2018]
 
Performance as User Experience [AEADC 2018]
Performance as User Experience [AEADC 2018]Performance as User Experience [AEADC 2018]
Performance as User Experience [AEADC 2018]
 
The Web Should Just Work for Everyone
The Web Should Just Work for EveryoneThe Web Should Just Work for Everyone
The Web Should Just Work for Everyone
 
Performance as User Experience [AEA SEA 2018]
Performance as User Experience [AEA SEA 2018]Performance as User Experience [AEA SEA 2018]
Performance as User Experience [AEA SEA 2018]
 
Performance as User Experience [An Event Apart Denver 2017]
Performance as User Experience [An Event Apart Denver 2017]Performance as User Experience [An Event Apart Denver 2017]
Performance as User Experience [An Event Apart Denver 2017]
 
Advanced Design Methods 1, Day 2
Advanced Design Methods 1, Day 2Advanced Design Methods 1, Day 2
Advanced Design Methods 1, Day 2
 
Advanced Design Methods 1, Day 1
Advanced Design Methods 1, Day 1Advanced Design Methods 1, Day 1
Advanced Design Methods 1, Day 1
 
Designing the Conversation [Paris Web 2017]
Designing the Conversation [Paris Web 2017]Designing the Conversation [Paris Web 2017]
Designing the Conversation [Paris Web 2017]
 
Exploring Adaptive Interfaces [Generate 2017]
Exploring Adaptive Interfaces [Generate 2017]Exploring Adaptive Interfaces [Generate 2017]
Exploring Adaptive Interfaces [Generate 2017]
 
Progressive Web Apps and the Windows Ecosystem [Build 2017]
Progressive Web Apps and the Windows Ecosystem [Build 2017]Progressive Web Apps and the Windows Ecosystem [Build 2017]
Progressive Web Apps and the Windows Ecosystem [Build 2017]
 
Writing for Engagement [TechReady 22]
Writing for Engagement [TechReady 22]Writing for Engagement [TechReady 22]
Writing for Engagement [TechReady 22]
 
Designing the Conversation [SmashingConf 2016]
Designing the Conversation [SmashingConf 2016]Designing the Conversation [SmashingConf 2016]
Designing the Conversation [SmashingConf 2016]
 
The Features of Highly Effective Forms [SmashingConf NYC 2016]
The Features of Highly Effective Forms [SmashingConf NYC 2016]The Features of Highly Effective Forms [SmashingConf NYC 2016]
The Features of Highly Effective Forms [SmashingConf NYC 2016]
 
Designing the Conversation [SpeechTek 2016]
Designing the Conversation [SpeechTek 2016]Designing the Conversation [SpeechTek 2016]
Designing the Conversation [SpeechTek 2016]
 

Recently uploaded

PCI PIN Basics Webinar from the Controlcase Team
PCI PIN Basics Webinar from the Controlcase TeamPCI PIN Basics Webinar from the Controlcase Team
PCI PIN Basics Webinar from the Controlcase Team
ControlCase
 
Elevating Tactical DDD Patterns Through Object Calisthenics
Elevating Tactical DDD Patterns Through Object CalisthenicsElevating Tactical DDD Patterns Through Object Calisthenics
Elevating Tactical DDD Patterns Through Object Calisthenics
Dorra BARTAGUIZ
 
Accelerate your Kubernetes clusters with Varnish Caching
Accelerate your Kubernetes clusters with Varnish CachingAccelerate your Kubernetes clusters with Varnish Caching
Accelerate your Kubernetes clusters with Varnish Caching
Thijs Feryn
 
Key Trends Shaping the Future of Infrastructure.pdf
Key Trends Shaping the Future of Infrastructure.pdfKey Trends Shaping the Future of Infrastructure.pdf
Key Trends Shaping the Future of Infrastructure.pdf
Cheryl Hung
 
Epistemic Interaction - tuning interfaces to provide information for AI support
Epistemic Interaction - tuning interfaces to provide information for AI supportEpistemic Interaction - tuning interfaces to provide information for AI support
Epistemic Interaction - tuning interfaces to provide information for AI support
Alan Dix
 
Designing Great Products: The Power of Design and Leadership by Chief Designe...
Designing Great Products: The Power of Design and Leadership by Chief Designe...Designing Great Products: The Power of Design and Leadership by Chief Designe...
Designing Great Products: The Power of Design and Leadership by Chief Designe...
Product School
 
Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024
Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024
Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024
Tobias Schneck
 
Software Delivery At the Speed of AI: Inflectra Invests In AI-Powered Quality
Software Delivery At the Speed of AI: Inflectra Invests In AI-Powered QualitySoftware Delivery At the Speed of AI: Inflectra Invests In AI-Powered Quality
Software Delivery At the Speed of AI: Inflectra Invests In AI-Powered Quality
Inflectra
 
Generating a custom Ruby SDK for your web service or Rails API using Smithy
Generating a custom Ruby SDK for your web service or Rails API using SmithyGenerating a custom Ruby SDK for your web service or Rails API using Smithy
Generating a custom Ruby SDK for your web service or Rails API using Smithy
g2nightmarescribd
 
Smart TV Buyer Insights Survey 2024 by 91mobiles.pdf
Smart TV Buyer Insights Survey 2024 by 91mobiles.pdfSmart TV Buyer Insights Survey 2024 by 91mobiles.pdf
Smart TV Buyer Insights Survey 2024 by 91mobiles.pdf
91mobiles
 
UiPath Test Automation using UiPath Test Suite series, part 3
UiPath Test Automation using UiPath Test Suite series, part 3UiPath Test Automation using UiPath Test Suite series, part 3
UiPath Test Automation using UiPath Test Suite series, part 3
DianaGray10
 
Monitoring Java Application Security with JDK Tools and JFR Events
Monitoring Java Application Security with JDK Tools and JFR EventsMonitoring Java Application Security with JDK Tools and JFR Events
Monitoring Java Application Security with JDK Tools and JFR Events
Ana-Maria Mihalceanu
 
GraphRAG is All You need? LLM & Knowledge Graph
GraphRAG is All You need? LLM & Knowledge GraphGraphRAG is All You need? LLM & Knowledge Graph
GraphRAG is All You need? LLM & Knowledge Graph
Guy Korland
 
Empowering NextGen Mobility via Large Action Model Infrastructure (LAMI): pav...
Empowering NextGen Mobility via Large Action Model Infrastructure (LAMI): pav...Empowering NextGen Mobility via Large Action Model Infrastructure (LAMI): pav...
Empowering NextGen Mobility via Large Action Model Infrastructure (LAMI): pav...
Thierry Lestable
 
Assuring Contact Center Experiences for Your Customers With ThousandEyes
Assuring Contact Center Experiences for Your Customers With ThousandEyesAssuring Contact Center Experiences for Your Customers With ThousandEyes
Assuring Contact Center Experiences for Your Customers With ThousandEyes
ThousandEyes
 
From Siloed Products to Connected Ecosystem: Building a Sustainable and Scala...
From Siloed Products to Connected Ecosystem: Building a Sustainable and Scala...From Siloed Products to Connected Ecosystem: Building a Sustainable and Scala...
From Siloed Products to Connected Ecosystem: Building a Sustainable and Scala...
Product School
 
Dev Dives: Train smarter, not harder – active learning and UiPath LLMs for do...
Dev Dives: Train smarter, not harder – active learning and UiPath LLMs for do...Dev Dives: Train smarter, not harder – active learning and UiPath LLMs for do...
Dev Dives: Train smarter, not harder – active learning and UiPath LLMs for do...
UiPathCommunity
 
JMeter webinar - integration with InfluxDB and Grafana
JMeter webinar - integration with InfluxDB and GrafanaJMeter webinar - integration with InfluxDB and Grafana
JMeter webinar - integration with InfluxDB and Grafana
RTTS
 
Leading Change strategies and insights for effective change management pdf 1.pdf
Leading Change strategies and insights for effective change management pdf 1.pdfLeading Change strategies and insights for effective change management pdf 1.pdf
Leading Change strategies and insights for effective change management pdf 1.pdf
OnBoard
 
AI for Every Business: Unlocking Your Product's Universal Potential by VP of ...
AI for Every Business: Unlocking Your Product's Universal Potential by VP of ...AI for Every Business: Unlocking Your Product's Universal Potential by VP of ...
AI for Every Business: Unlocking Your Product's Universal Potential by VP of ...
Product School
 

Recently uploaded (20)

PCI PIN Basics Webinar from the Controlcase Team
PCI PIN Basics Webinar from the Controlcase TeamPCI PIN Basics Webinar from the Controlcase Team
PCI PIN Basics Webinar from the Controlcase Team
 
Elevating Tactical DDD Patterns Through Object Calisthenics
Elevating Tactical DDD Patterns Through Object CalisthenicsElevating Tactical DDD Patterns Through Object Calisthenics
Elevating Tactical DDD Patterns Through Object Calisthenics
 
Accelerate your Kubernetes clusters with Varnish Caching
Accelerate your Kubernetes clusters with Varnish CachingAccelerate your Kubernetes clusters with Varnish Caching
Accelerate your Kubernetes clusters with Varnish Caching
 
Key Trends Shaping the Future of Infrastructure.pdf
Key Trends Shaping the Future of Infrastructure.pdfKey Trends Shaping the Future of Infrastructure.pdf
Key Trends Shaping the Future of Infrastructure.pdf
 
Epistemic Interaction - tuning interfaces to provide information for AI support
Epistemic Interaction - tuning interfaces to provide information for AI supportEpistemic Interaction - tuning interfaces to provide information for AI support
Epistemic Interaction - tuning interfaces to provide information for AI support
 
Designing Great Products: The Power of Design and Leadership by Chief Designe...
Designing Great Products: The Power of Design and Leadership by Chief Designe...Designing Great Products: The Power of Design and Leadership by Chief Designe...
Designing Great Products: The Power of Design and Leadership by Chief Designe...
 
Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024
Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024
Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024
 
Software Delivery At the Speed of AI: Inflectra Invests In AI-Powered Quality
Software Delivery At the Speed of AI: Inflectra Invests In AI-Powered QualitySoftware Delivery At the Speed of AI: Inflectra Invests In AI-Powered Quality
Software Delivery At the Speed of AI: Inflectra Invests In AI-Powered Quality
 
Generating a custom Ruby SDK for your web service or Rails API using Smithy
Generating a custom Ruby SDK for your web service or Rails API using SmithyGenerating a custom Ruby SDK for your web service or Rails API using Smithy
Generating a custom Ruby SDK for your web service or Rails API using Smithy
 
Smart TV Buyer Insights Survey 2024 by 91mobiles.pdf
Smart TV Buyer Insights Survey 2024 by 91mobiles.pdfSmart TV Buyer Insights Survey 2024 by 91mobiles.pdf
Smart TV Buyer Insights Survey 2024 by 91mobiles.pdf
 
UiPath Test Automation using UiPath Test Suite series, part 3
UiPath Test Automation using UiPath Test Suite series, part 3UiPath Test Automation using UiPath Test Suite series, part 3
UiPath Test Automation using UiPath Test Suite series, part 3
 
Monitoring Java Application Security with JDK Tools and JFR Events
Monitoring Java Application Security with JDK Tools and JFR EventsMonitoring Java Application Security with JDK Tools and JFR Events
Monitoring Java Application Security with JDK Tools and JFR Events
 
GraphRAG is All You need? LLM & Knowledge Graph
GraphRAG is All You need? LLM & Knowledge GraphGraphRAG is All You need? LLM & Knowledge Graph
GraphRAG is All You need? LLM & Knowledge Graph
 
Empowering NextGen Mobility via Large Action Model Infrastructure (LAMI): pav...
Empowering NextGen Mobility via Large Action Model Infrastructure (LAMI): pav...Empowering NextGen Mobility via Large Action Model Infrastructure (LAMI): pav...
Empowering NextGen Mobility via Large Action Model Infrastructure (LAMI): pav...
 
Assuring Contact Center Experiences for Your Customers With ThousandEyes
Assuring Contact Center Experiences for Your Customers With ThousandEyesAssuring Contact Center Experiences for Your Customers With ThousandEyes
Assuring Contact Center Experiences for Your Customers With ThousandEyes
 
From Siloed Products to Connected Ecosystem: Building a Sustainable and Scala...
From Siloed Products to Connected Ecosystem: Building a Sustainable and Scala...From Siloed Products to Connected Ecosystem: Building a Sustainable and Scala...
From Siloed Products to Connected Ecosystem: Building a Sustainable and Scala...
 
Dev Dives: Train smarter, not harder – active learning and UiPath LLMs for do...
Dev Dives: Train smarter, not harder – active learning and UiPath LLMs for do...Dev Dives: Train smarter, not harder – active learning and UiPath LLMs for do...
Dev Dives: Train smarter, not harder – active learning and UiPath LLMs for do...
 
JMeter webinar - integration with InfluxDB and Grafana
JMeter webinar - integration with InfluxDB and GrafanaJMeter webinar - integration with InfluxDB and Grafana
JMeter webinar - integration with InfluxDB and Grafana
 
Leading Change strategies and insights for effective change management pdf 1.pdf
Leading Change strategies and insights for effective change management pdf 1.pdfLeading Change strategies and insights for effective change management pdf 1.pdf
Leading Change strategies and insights for effective change management pdf 1.pdf
 
AI for Every Business: Unlocking Your Product's Universal Potential by VP of ...
AI for Every Business: Unlocking Your Product's Universal Potential by VP of ...AI for Every Business: Unlocking Your Product's Universal Potential by VP of ...
AI for Every Business: Unlocking Your Product's Universal Potential by VP of ...
 

Getting Started with Progressive Web Apps [Beyond Tellerrand 2019]

  • 1. Getting started with Aaron Gustafson @AaronGustafson noti.st/AaronGustafson
  • 2. @AaronGustafson Follow along āš Slides: https://aka.ms/btconf-pwa-slides āš Work files: https://aka.ms/btconf-pwa-code āš Final Demo: https://aka.ms/btconf-pwa-live
  • 5. What exactly is a Progressive Web App?
  • 6. “Progressive Web App” is a marketing term Progressive Web App
  • 13. @AaronGustafson What’s a PWA, technically? HTTPS Web App Manifest
  • 14. @AaronGustafson What’s a PWA, technically? HTTPS Web App Manifest Service Worker
  • 17. Starbucks: 2x increase in daily active users aka.ms/google-io-2018
  • 18. Tinder: Core experience with 90% less code aka.ms/tinder-pwa-2017
  • 19. Trivago: 97% increase in click-outs to hotel offers aka.ms/trivago-pwa-2017
  • 20. West Elm: 15% increase in time on site 9% increase in revenue per visit aka.ms/west-elm-pwa-2017
  • 21.
  • 22. PWAs start with a great web experience and then enhance that experience for performance, resilience, installation, and engagement
  • 23. PWAs start with a great web experience and then enhance that experience for performance, resilience, installation, and engagement
  • 28. @AaronGustafson What’s a PWA, technically? HTTPS Web App Manifest Service Worker
  • 30. @AaronGustafson HTTPS is simple (& free) now āš Many hosts include it āš GitHub āš Netlify āš AWS āš etc. āš LetsEnrcypt & Certbot for everything else https://letsencrypt.org/
  • 31. @AaronGustafson What’s a PWA, technically? HTTPS Web App Manifest Service Worker
  • 33. @AaronGustafson Manifest files are JSON files { "property_a": "value", "property_b": ["value_1", "value_2"], "property_c": { "nested_property": "nested_value" } }
  • 34. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 35. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 36. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "dir": "ltr", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 37. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "dir": "auto", // default "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 38. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 39. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 40. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 41. @AaronGustafson Minimum Viable Manifest { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 42. @AaronGustafson Reference in the head <link rel="manifest" href="/manifest.json">
  • 43. @AaronGustafson Reference in the head <link rel="manifest" href="/manifest.json">
  • 44. @AaronGustafson Reference in the head <link rel="manifest" href="/manifest.json">
  • 45. @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, 2. name, 3. short_name, and 4. description.
  • 46. @AaronGustafson Yours should look like this { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 47. @AaronGustafson Let’s prep for installâ€Ļ { "lang": "en-US", "name": "Aaron Gustafson", "short_name": "AaronG", "description": "The online home and work of Aaron Gustafson." }
  • 48. @AaronGustafson Where do we begin? { â€Ļ "start_url": "/" }
  • 49. @AaronGustafson Where does this apply? { â€Ļ "start_url": "/", "scope": "/" // defaults to the start_url value }
  • 50. @AaronGustafson What should it look like? { â€Ļ "start_url": "/", "display": "minimal-ui" }
  • 51. @AaronGustafson Display Modes "display": "browser" "display": "standalone" "display": "minimal-ui""display": "fullscreen"
  • 52. @AaronGustafson Need to lock orientation? { â€Ļ "start_url": "/", "display": "minimal-ui", "orientation": "any" }
  • 53. @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/
  • 54. @AaronGustafson A little colorâ€Ļ { â€Ļ "start_url": "/", "display": "minimal-ui", "orientation": "any", "theme_color": "#27831B" }
  • 55. @AaronGustafson A little colorâ€Ļ { â€Ļ "start_url": "/", "display": "minimal-ui", "orientation": "any", "theme_color": "#27831B", "background_color": "#fffcf4" }
  • 56. @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" } ] }
  • 57. @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" } ] }
  • 58. @AaronGustafson Anatomy of an ImageResource { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600" }
  • 59. @AaronGustafson Anatomy of an ImageResource { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600" }
  • 60. @AaronGustafson Anatomy of an ImageResource { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600" }
  • 61. @AaronGustafson Anatomy of an ImageResource { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600" }
  • 62. @AaronGustafson Anatomy of an ImageResource { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600" }
  • 63. @AaronGustafson Anatomy of an ImageResource { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600", "purpose": "badge" }
  • 64. @AaronGustafson Anatomy of an ImageResource { "src": "/i/og-logo.png", "type": "image/png", "sizes": "800x600", "purpose": "maskable" }
  • 65. @AaronGustafson Let‘s improve our manifest! āš Add the following Manifest members: 1. start_url, 2. display, 3. theme_color, 4. background_color, and 5. icons Suggested sizes: 48x48, 72x72, 96x96, 144x144, 192x192, and 512x512
  • 68. How’d it go? Any questions?
  • 70. @AaronGustafson What’s a PWA, technically? HTTPS Web App Manifest Service Worker
  • 72. @AaronGustafson Registering a Service Worker if ( "serviceWorker" in navigator ) { navigator.serviceWorker.register( "/serviceworker.min.js" ); }
  • 73. @AaronGustafson Registering a Service Worker if ( "serviceWorker" in navigator ) { navigator.serviceWorker.register( "/serviceworker.min.js" ); }
  • 74. @AaronGustafson Registering a Service Worker if ( "serviceWorker" in navigator ) { navigator.serviceWorker.register( "/serviceworker.min.js" ); }
  • 75. @AaronGustafson Registering a Service Worker if ( "serviceWorker" in navigator ) { navigator.serviceWorker.register( "/serviceworker.min.js" ); }
  • 76. @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 ); }); }
  • 78. @AaronGustafson The Service Worker Lifecycle Browser Install Activation Ready aka.ms/pwa-lifecycle
  • 79. @AaronGustafson Listening for these events self.addEventListener( "install", function( event ){ console.log( "installing" ); });
  • 80. @AaronGustafson Listening for these events self.addEventListener( "install", function( event ){ console.log( "installing" ); });
  • 81. @AaronGustafson Listening for these events self.addEventListener( "install", function( event ){ console.log( "installing" ); });
  • 82. @AaronGustafson Listening for these events self.addEventListener( "install", function( event ){ console.log( "installing" ); });
  • 83. @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
  • 84. @AaronGustafson Yours should look similar self.addEventListener( "install", function( event ){ console.log( "installing" ); }); self.addEventListener( "activate", function( event ){ console.log( "activating" ); });
  • 85. @AaronGustafson The Service Worker Lifecycle Browser Install Activation Ready aka.ms/pwa-lifecycle
  • 86. @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" ]); }) ); });
  • 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" ]); }) ); });
  • 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" ]); }) ); });
  • 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" ]); }) ); });
  • 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" ]); }) ); });
  • 91. @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" ]); }) ); });
  • 92. @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" ]); }) ); });
  • 93. @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?
  • 94. @AaronGustafson We need to clean up 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 ); }) ); }) ); });
  • 95. @AaronGustafson We need to clean up 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 ); }) ); }) ); });
  • 96. @AaronGustafson We need to clean up 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 ); }) ); }) ); });
  • 97. @AaronGustafson We need to clean up 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 ); }) ); }) ); });
  • 98. @AaronGustafson We need to clean up 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 ); }) ); }) ); });
  • 99. @AaronGustafson We need to clean up 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 ); }) ); }) ); });
  • 100. @AaronGustafson Let‘s clean up āš Leverage the activate event to clear stale caches āš Load your page in a browser āš What happened?
  • 101. @AaronGustafson Use the new 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(); });
  • 102. @AaronGustafson Take over 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(); });
  • 103. @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?
  • 106. @AaronGustafson How requests are made Browser Internet
  • 107. @AaronGustafson Along comes Service Worker Browser Internet
  • 108. @AaronGustafson Along comes Service Worker Browser Internet Cache
  • 109. @AaronGustafson Along comes Service Worker Browser Internet Cache 👎đŸģ
  • 110. @AaronGustafson Intercepting requests self.addEventListener( "fetch", function( event ){ console.log( "fetching" ); });
  • 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?
  • 112. @AaronGustafson We can issue our own fetch self.addEventListener( "fetch", function( event ){ event.respondWith( fetch( event.request ) ); });
  • 113. @AaronGustafson We can issue our own fetch self.addEventListener( "fetch", function( event ){ event.respondWith( fetch( event.request ) ); });
  • 115. @AaronGustafson What if the request fails? const VERSION = "v1", OFFLINE_PAGE = "offline.html";
  • 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 ]); }) ); });
  • 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 ); }) ); } });
  • 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 ); }) ); } });
  • 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 ); }) ); } });
  • 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 ); }) ); } });
  • 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
  • 122. @AaronGustafson Caching strategies āš Network → cache → offline āš Cache → network → offline āš Cache vs network race → cache → offline āš etc.
  • 123. @AaronGustafson Network, then cache if ( event.request.mode === "navigate" ) { event.respondWith( fetch( event.request ) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }
  • 124. @AaronGustafson Network, then cache self.addEventListener( "fetch", function( event ){ let request = event.request, url = request.url; // all the rest of the code }
  • 125. @AaronGustafson Network, then cache if ( event.request.mode === "navigate" ) { event.respondWith( fetch( event.request ) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }
  • 126. @AaronGustafson Network, then cache if ( request.mode === "navigate" ) { event.respondWith( fetch( request ) .catch(error => { return caches.match( OFFLINE_PAGE ); }) ); }
  • 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 ); }) ); }
  • 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 ); }) ); }
  • 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 ); }) ); }
  • 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 ); }) ); }
  • 131. @AaronGustafson Network, then cache .catch(error => { return caches.match( OFFLINE_PAGE ); })
  • 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 ); }); })
  • 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 ); }); })
  • 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 ); }); })
  • 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 ); }); }) ); }
  • 136. @AaronGustafson Caching strategies ✓ Network → cache → offline āš Cache → network → offline āš Cache vs network race → cache → offline āš etc.
  • 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." }) ); }) ); }
  • 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." }) ); }) ); }
  • 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." }) ); }) ); }
  • 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." }) ); }) ); }
  • 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." }) ); }) ); }
  • 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." }) ); }) ); }
  • 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(); })
  • 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(); }
  • 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(); }
  • 147. @AaronGustafson And we can use it like this return fetch( request ) .then( response => cacheResponse( response, event ) )
  • 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 ); }); }) ); }
  • 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." }) ); }) ); }
  • 150. @AaronGustafson Caching strategies ✓ Network → cache → offline ✓ Cache → network → offline āš Cache vs network race → cache → offline āš etc.
  • 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
  • 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
  • 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
  • 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
  • 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?
  • 157. Could we save data?
  • 158. @AaronGustafson Look at the connection var 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(); } }
  • 159. @AaronGustafson Look at the connection var 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(); } }
  • 160. @AaronGustafson Look at the connection var 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(); } }
  • 161. @AaronGustafson Look at the connection var 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(); } }
  • 162. @AaronGustafson Look at the connection self.addEventListener( "fetch", function( event ){ testConnection(); let request = event.request, url = request.url; â€Ļ });
  • 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 } }) ); }
  • 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 } }) ); }
  • 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 } }) ); }
  • 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 } }) ); }
  • 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 } }) ); }
  • 168. @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 } }) ); }
  • 169. @AaronGustafson Dynamic images? Yes please! const VERSION = "v2", OFFLINE_PAGE = "offline.html", SVG_OFFLINE = '<svg â€Ļ></svg>', SVG_SLOW = '<svg â€Ļ></svg>';
  • 170. @AaronGustafson Dynamic images? Yes please! function newImageResponse( svg ) { return new Response( svg, { headers: { 'Content-Type': 'image/svg+xml' } }); }
  • 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( // Respond with an “offline” image ); } else { // Respond with a “saving data” image } }) ); }
  • 172. @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( () => newImageResponse( SVG_OFFLINE ) ); } else { return newImageResponse( SVG_SLOW ); } }) ); }
  • 173. @AaronGustafson Let‘s discuss āš What other ways we could use Service Workers to improve the user experience?
  • 175. @AaronGustafson Bonus content! āš Share Target āš Shortcuts (coming soon)
  • 176. PWAs start with a great web experience and then enhance that experience for performance, resilience, installation, and engagement
  • 177. PWAs start with a great web experience and then enhance that experience for performance, resilience, installation, and engagement
  • 178. @AaronGustafson Congrats, you made a PWA! HTTPS Web App Manifest Service Worker

Editor's Notes

  1. Hi there, my name is Aaron Gustafson Pronouns he/him/his WaSP Microsoft - Web Standards & a11y Thank you to Jeffrey & Eric for having me and to Toby, Marci, Mike and all of the staff that keep this conference running so smoothly.
  2. I’m sure many of you may be wondering: What exactly is a PWA? (apart from yet another acronym)
  3. PWA stands for Progressive Web App. And, believe me, you’re not alone in asking this question.
  4. Lots of very smart people are wrestling with it right now.
  5. When it comes down to itâ€Ļ
  6. I think one of the things about the term progressive web app that trips folks up is the “web app” bit. What is a web app anyway? Jeremy Keith likes to say “Like obscenity and brunch, web apps can be described but not defined.”
  7. What’s important to realize, however is that any web project can (and probably should be) a progressive web app.
  8. In fact, you might as well think of these as progressiveâ€Ļ
  9. Web sites.
  10. PWAs have a lot of support from the browser world and at this point, most of the major capabilities of PWAs—with some exceptions like Push on iOS—are available in all modern browsers.
  11. As someone who builds websites, “PWA” as a marketing term isn’t terribly helpful. So let’s look at a what it takes to turn your site into a PWA. It starts with making your site secure. Most of the APIs you will need access to are only available under SSL.
  12. Next, you’ll need to add a Web App Manifest to your site. This contains a lot of the ”meta” information about your site that can be useful should a user choose to install it.
  13. The final requirement for a MVPWA is a Service Worker. I am going to hold off on discussing Service Workers for a moment, but we’ll circle back to them momentarily.
  14. So there’s a lot of hype around PWAs. Should you believe it?
  15. Maybe? Probably. Here are a few tidbits you might find interesting.
  16. There are a ton of success stories out there.
  17. I higly recommend checking out PWA stats.com, from the folks at Cloud Four, for more.
  18. In truthâ€Ļ
  19. The key word here is enhance
  20. Combine enhance with the first word in PWA—progressive—and you arrive at the age old philosophy of
  21. Progressive enhancement, which is really at the core of progressive web apps
  22. So what does that actually look like? Let’s tuck into itâ€Ļ
  23. So we’ve got a normal site that can become a PWA.
  24. So back to our MVPWA. I mentioned that I would come back to Service Worker, so let’sâ€Ļ
  25. So back to our MVPWA. I mentioned that I would come back to Service Worker, so let’sâ€Ļ
  26. So back to our MVPWA. I mentioned that I would come back to Service Worker, so let’sâ€Ļ
  27. So back to our MVPWA. I mentioned that I would come back to Service Worker, so let’sâ€Ļ
  28. Everything is optional, butâ€Ļ
  29. First off, you can define the language. If you have a multi-lingual app, you might consider providing multiple manifests tailored to each as there is—as of yet—no way of managing translations within the manifest.
  30. If you want, you can define a writing direction like “ltr” for left to right or “rtl” for right to left, but you could alsoâ€Ļ
  31. Set it to optional, which is the default.
  32. In which case, you don’t really need it.
  33. Name is the preferred name for the app. Short name enables you to define a specific short option just in case the host operating system doesn’t support as many characters as you need for your app’s name. Better to not leave it to chance.
  34. Description says what the site or app does.
  35. We’ll come back this this more in a moment.
  36. The next step is to connect this file to your site via HTML, using the link tag
  37. The relationship of the linked document is ”manifest”
  38. And the href should point to your JSON file. It’s worth notingâ€Ļ
  39. 5-7 min
  40. This is the page that should open when your app is launched after install
  41. This is the page that should open when your app is launched after install
  42. There are a few modes available hereâ€Ļ
  43. This can be overridden via JS as well using the Screen Orientation API
  44. You can see the title bar is colored
  45. Here the background is applied to the splash screenâ€Ļ But what about this icon?
  46. Three different options
  47. The src is the location of your source file (can be relative to the manifest URL) Any image format is possible, though SVG is not well supported (yet)
  48. MIME declaration for the image to enable browser to select based on support
  49. Space-separated list of dimensions (width x height) supported by the image. ICO files can have multiple, otherwise it should be the natural dimensions.
  50. 5-7 min
  51. So we’ve got a normal site that can become a PWA.
  52. Or to break it down further
  53. 5-7 min
  54. 5-7 min
  55. So back to our MVPWA. I mentioned that I would come back to Service Worker, so let’sâ€Ļ
  56. Focus on that. A special kind of web worker (it runs in it’s own thread) Concerned with reducing network dependence Your own personal “man in the middle”
  57. In order to take advantage of this, you need to register a service worker in the browser.
  58. First test for the feature
  59. Then register the worker by pointing at the Service Worker JavaScript file you’ve created.
  60. The path is very important. If you want to run your service worker for the whole site, it must be in the root directory, NOT your javascript subdirectory. If it is in a subdirectory, it can only run against requests for assets in that subdirectory or deeper.
  61. The registration returns a Promise, so you can act on it.
  62. The great thing about this is that it’s an enhancement. The site will work fine without Service Worker as well, but it works better with it.
  63. A Service Worker has a lifecycle. There is a lot of nuance to it, but here’s the Cliff’s Notes versionâ€Ļ The Service Worker is registered with the browser. Then it goes through the install process. You can use this opportunity to pre-cache some assets if you like. Once installed, it’s activated. This is generally a good time to clean up any old caches. Then it’s ready to use, but it will not actually do anything until the next page is loaded. I’m going to quickly go through some examples of how Service Workers can operate, but it’s important to remember you are in total control of their behavior.
  64. 5-7 min
  65. Install is a great time to download key resources & add them to the cache
  66. 5-7 min
  67. Here I’m actually using ES6 syntax with the fat arrow function. ES6 is allowable in Service Worker because all Service Worker-supporting browsers also support the more modern JavaScript syntax
  68. 5-7 min
  69. Normally an installed ServiceWorker will wait until the next page load to activate. This skips the waiting.
  70. And this allows you to claim any open clients (windows, tabs, etc.) and apply the service worker to them.
  71. 5-7 min
  72. 5-7 min
  73. In a traditional networking scenarioâ€Ļ
  74. When a Service Worker is in play, it sits between the browser and the network.
  75. It also has programmatic access to a local cache. It can intercept network requests from the browser and respond with items in the cache without ever going out to the network. Say, for instance, requests for CSS and JavaScript files you pre-cached during install
  76. You could also set up rules to tell the SW to go to the cache first, but fall back to the network if a cached copy of the resource is not available. Then you can keep a copy for next time too. Caching is key to improved performance and good offline experiences, which are the hallmark of a PWA.
  77. 5-7 min
  78. But that’s not terribly interesting
  79. But that’s not terribly interesting
  80. Navigation only
  81. This you saw before.
  82. But now we catch errors and respond from the cache
  83. 5-7 min
  84. Different approaches for different needs
  85. We just looked at thisâ€Ļ what if we want to try the network and fall back to a cached version of the page?
  86. I’m going to make the code a little easier to read by extracting two useful pieces of data, the request and it’s URL
  87. So thisâ€Ļ
  88. Becomes this
  89. Then, before we can actually respond from the cache, we need to add HTML to the cache First, we insert a then() block, which gets the response object
  90. And cache it, using the request as a key
  91. And then we return the response
  92. Now we can focus on the catch() block
  93. Now we can focus on the catch() block
  94. Now we can focus on the catch() block
  95. Now we can focus on the catch() block
  96. Now we can focus on the catch() block
  97. Now we can focus on the catch() block
  98. Different approaches for different needs
  99. Technically, this is an elseâ€Ļif
  100. First we check for a cached result and we send it if we have it
  101. Questions?
  102. Questions?
  103. Questions?
  104. Questions?
  105. Questions?
  106. Now we can focus on the catch() block
  107. Questions?
  108. Different approaches for different needs
  109. Questions?
  110. Questions?
  111. Questions?
  112. Questions?
  113. Cache first with a server refresh in the background (for next time), falling back to the network, then offline
  114. New conditional for images
  115. Respond with cache match first
  116. If all is well
  117. Otherwise, we need a fallback
  118. Incidentally, we can use this same approach as a fallback for a fail state.
  119. Adding two new constants
  120. And a new function to generate an svg response
  121. This
  122. Becomes this, passing in the appropriate SVG
  123. Service workers even have their own levels of enhancement CLICK And more will come with time, like background sync.
  124. As I mentioned beforeâ€Ļ CLICK
  125. start with a great web experience
  126. Thank you so much for your time and attention!