by Jason Lengstorf
@jlengstorf | jason@lengstorf.com
Slides: git.io/v9tEr
Source: Time To Interactive from Lighthouse. I ran the test twice and took the faster time.
@UnaYes. On a recent trip to the US I spent $7 to load the conference homepage on my iPhone.
— Roy Tomeij (@roy) May 8, 2017
Price in USD for a US-based AT&T customer traveling internationally.
Source: HTTP Archive
...and what if the connection is flaky?
(And it doesn’t mean starting from scratch.)
# Get a local copy of the starter code.
git clone git@github.com:jlengstorf/pwa-simple.git
# Move into the cloned directory.
cd pwa-simple/
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./service-worker.js')
.then(() => {
console.log(`ServiceWorker registered!`);
})
.catch(error => {
console.error('ServiceWorker error:', error);
})
}
</script>
NOTE: The Service Worker must be created at the root domain.
Example: https://example.org/service-worker.js
const CACHE_VERSION = 'sample-pwa-v1';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_VERSION).then(cache => {
// Download all required resources to render the app.
return cache.addAll([
'./index.html',
'./scripts.js',
'./styles.css',
]);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches
// Check for cached data.
.match(event.request)
// Return the cached data if it exists.
.then(data => data)
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches
// Check for cached data.
.match(event.request)
- // Return the cached data if it exists.
- .then(data => data)
+ // Return the cached data OR hit the network.
+ .then(data => data || fetch(event.request))
);
});
self.addEventListener('fetch', event => {
+ const fetchAndCache = request =>
+ caches.open(CACHE_VERSION).then(cache =>
+ // Load the response from the network.
+ fetch(request).then(response => {
+ // Add the response to the cache.
+ cache.put(request, response.clone());
+ return response;
+ })
+ );
event.respondWith(
/* the rest of the code is skipped to save space */
});
self.addEventListener('fetch', event => {
const fetchAndCache = request =>
/* the rest of the code is skipped to save space */
event.respondWith(
caches
// Check for cached data.
.match(event.request)
// Return the cached data OR hit the network.
- .then(data => data || fetch(event.request))
+ .then(data => data || fetchAndCache(event.request))
);
});
const CACHE_VERSION = 'sample-pwa-v1';
+ const OFFLINE_IMAGE = './offline.png';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_VERSION).then(cache => {
// Download all required resources to render the app.
return cache.addAll([
'./index.html',
'./scripts.js',
'./styles.css',
+ OFFLINE_IMAGE,
]);
})
);
});
event.respondWith(
caches
// Check for cached data.
.match(event.request)
// Return the cached data OR hit the network.
.then(data => data || fetchAndCache(event.request))
+ .catch(() => {
+ const url = new URL(event.request.url);
+
+ // Show the fallback image for failed GIF requests.
+ if (url.pathname.match(/\.gif$/)) {
+ return caches.match(OFFLINE_IMAGE);
+ }
+ })
);
const CACHE_VERSION = 'sample-pwa-v1';
const OFFLINE_IMAGE = './offline.png';
+ const OFFLINE_PAGE = './offline.html';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_VERSION).then(cache => {
// Download all required resources to render the app.
return cache.addAll([
'./index.html',
'./scripts.js',
'./styles.css',
OFFLINE_IMAGE,
+ OFFLINE_PAGE,
]);
})
);
});
.catch(() => {
const url = new URL(event.request.url);
// Show the fallback image for failed GIF requests.
if (url.pathname.match(/\.gif$/)) {
return caches.match(OFFLINE_IMAGE);
}
+ // Show an offline page for other failed requests.
+ return caches.match(OFFLINE_PAGE);
})
{
"name": "Progressive Web App Demo for Web Summer Camp 2017",
"short_name": "#websc PWA",
"start_url": "./index.html",
"display": "standalone",
"background_color": "#faf8fd",
"theme_color": "#663399"
}
"theme_color": "#663399",
+ "icons": [
+ {
+ "src": "./pwa-icon.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "./pwa-icon-512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ]
}
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <meta name="theme-color" content="#663399">
+ <link rel="manifest" href="./manifest.json">
<link rel="stylesheet" href="./styles.css">
<title>Simple Progressive Web App</title>
</head>
sw-precache
# clone the source repo
git clone git@github.com:jlengstorf/pwa-workshop-starter.git
# move into the app directory
cd pwa-workshop-starter
# install dependencies with yarn
npm install
Lighthouse is Google’s plugin to audit apps for PWA features.
sw-precache-webpack-plugin
npm install -D sw-precache-webpack-plugin
sw-precache
tool from the Google Chrome teamsw-precache-webpack-plugin
:plugins: [
new SWPrecacheWebpackPlugin(
{
filepath: './dist/service-worker.js',
runtimeCaching: [{
urlPattern: /[.]jpg$/,
handler: 'cacheFirst'
}],
staticFileGlobs: [
'dist/assets/{css,js}/main.{css,js}',
'dist/assets/*.{html,png,xml,ico,svg}'
],
stripPrefix: 'dist/',
}
),
],
<script>
(function() {
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('./service-worker.js')
.then(reg => {
console.log('Added Service Worker at', reg.scope);
})
.catch(error => {
console.error(error);
});
}
})();
</script>
NOTE: We can automate this with an NPM package.
Technically, we’re all done at this point.
responsive-lazyload.js
:<a href="img/bacon.jpg"
class="gallery__link js--lazyload">
<img class="gallery__image"
src="img/thumbs/bacon.jpg"
srcset="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
data-lazyload="img/thumbs/bacon.jpg 1x,
img/thumbs/bacon@2x.jpg 2x"
alt="image">
</a>
All with a few minutes of effort.
And with just a bit of extra work, we can lazy load resources to further improve load times.
Tweet: @jlengstorf #websc