How to obtain maximum performance from your Progressive web app
In this talk i want to explain what PWA is and how to obtain maximum performance for your project focusing on the caching strategies, use cases when you can prefer your caching data vs use cases when it's better to try to fetch your data first.
6. PWA Caching Strategies – Gabriele Falasca
Migrate an existing webapp to pwa
1. HTTPS
2. THINK ABOUT CACHING
3. SERVICE WORKER
4. ADD TO HOMESCREEN
5. USE THE APIs YOU NEED
6. ENJOY
8. PWA Caching Strategies – Gabriele Falasca
SSR – Server Side Rendering
1. SERVER RETURNS A COMPLETE STATIC PAGE
2. PAGE LOADS REMOTE CSS, JS AND ASSETS
3. EVERY ACTION ON THE PAGE CAUSES A BROWSER
RELOAD
9. PWA Caching Strategies – Gabriele Falasca
CSR – CLIENT Side Rendering
1. SERVER RETURNS AN EMPTY PAGE (TEMPLATE)
2. JS MAKES REQUESTS TO GET PAGE CONTENT
3. EVERY ACTION ON THE PAGE RELOAD ONLY THE
DYNAMIC CONTENT
10. PWA Caching Strategies – Gabriele Falasca
SSR caching strategies
1. CACHE CSS JS AND ASSETS
2. IF THE APP HAS A STATIC PAGE, CACHE IT
TOO
11. PWA Caching Strategies – Gabriele Falasca
CSR caching strategies
1. CACHE CSS, JS, TEMPLATE AND ASSETS
2. CACHE DATA USING CACHE API OR INDEXEDDB
3. DETECT OFFLINE STATE THROUGH
JAVASCRIPT
12. PWA Caching Strategies – Gabriele Falasca
Detect offline state - JS
if(navigator.offline)
OR
navigator.addEventListener(‘offline’, …)
navigator.addEventListener(‘online’, …)
14. PWA Caching Strategies – Gabriele Falasca
What you can do with Service Workers?
1. CACHING THINGS
2. REGISTER FOR PUSH NOTIFICATION
3. USE VARIOUS API
4. AND MANY MANY MORE…
17. PWA Caching Strategies – Gabriele Falasca
WHEN TO CACHE RESOURCES?
on install
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open(CACHE_NAME).then(function (cache) {
console.log('caching App shell');
return cache.addAll([
'/static/js/main.chunk.js',
...pokemonImagesArray(),
]);
})
);
});
Ideal for: CSS, images, fonts, JS, templates… basically anything you'd consider static to that
"version" of your site.
18. PWA Caching Strategies – Gabriele Falasca
WHEN TO CACHE RESOURCES?
on activate
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.filter(function(cacheName) {
// Return true if you want to remove this cache,
// but remember that caches are shared across
// the whole origin
}).map(function(cacheName) {
return caches.delete(cacheName);
})
);
})
);
});
Ideal for: Clean-up & migration, or when you have to change your IndexedDb schema
YES, THERE IS A DB ALSO ON FRONTEND!
19. PWA Caching Strategies – Gabriele Falasca
WHEN TO CACHE RESOURCES?
on user interaction
document.querySelector(‘#cache-this!').addEventListener('click',
function(event) {
event.preventDefault();
var id = this.someDataset.articleId;
caches.open('mysite-article-' + id).then(function(cache) {
fetch('/get-article-urls?id=' + id).then(function(response) {
// /get-article-urls returns a JSON-encoded array of
// resource URLs that a given article depends on
return response.json();
}).then(function(urls) {
cache.addAll(urls);
});
});
});
Ideal for: “Read later”
Cache API is available not only in service workers!
20. PWA Caching Strategies – Gabriele Falasca
WHEN TO CACHE RESOURCES?
on network response
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open(CACHE_NAME).then(function(cache) {
return cache.match(event.request).then(function (response) {
return response || fetch(event.request).then(function(response) {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});
Ideal for: To allow for efficient memory usage, you can only read a response/request's body once.
.clone() is used to create additional copies that can be read separately.
22. PWA Caching Strategies – Gabriele Falasca
WHEN TO CACHE RESOURCES?
on background sync
self.addEventListener('sync', function(event) {
if (event.id == 'update-leaderboard') {
event.waitUntil(
caches.open(CACHE_NAME).then(function(cache) {
return cache.add('/leaderboard.json');
})
);
}
});
Ideal for: Content relating to a notification, such as a chat message, a breaking news story, or an email. Also
infrequently changing content that benefits from immediate sync, such as a todo list update or a calendar
alteration.
23. PWA Caching Strategies – Gabriele Falasca
WHEN TO CACHE RESOURCES?
stale-while-revalidate
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open(CACHE_NAME).then(function(cache) {
return cache.match(event.request).then(function(response) {
var fetchPromise = fetch(event.request).then(function(networkResponse) {
cache.put(event.request, networkResponse.clone());
return networkResponse;
})
return response || fetchPromise;
})
})
);
});
If there's a cached version available, use it, but fetch an update for next time.
Ideal for: Frequently updating resources where having the very latest version is non-essential. Like avatars.
25. PWA Caching Strategies – Gabriele Falasca
RESPONDING TO REQUESTS
network only
self.addEventListener('fetch', function(event) {
event.respondWith(fetch(event.request));
// or simply don't call event.respondWith, which
// will result in default browser behaviour
});
Ideal for: Every non-GET request :)
26. PWA Caching Strategies – Gabriele Falasca
RESPONDING TO REQUESTS
cache, network fallback
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});
Ideal for: If you're building offline-first, this is how you'll handle the majority of requests. Other patterns will be
exceptions based on the incoming request.
27. PWA Caching Strategies – Gabriele Falasca
RESPONDING TO REQUESTS
network, cache fallback
self.addEventListener('fetch', function(event) {
event.respondWith(
fetch(event.request).catch(function() {
return caches.match(event.request);
})
);
});
Ideal for: A quick-fix for resources that update frequently, outside of the "version" of the site. E.g. articles, avatars,
social media timelines, game leader boards
28. PWA Caching Strategies – Gabriele Falasca
RESPONDING TO REQUESTS
cache, then network
// IN THE PAGE
var networkDataReceived = false;
startSpinner();
// fetch fresh data
var networkUpdate = fetch('/data.json').then(function(response) {
return response.json();
}).then(function(data) {
networkDataReceived = true;
updatePage(data);
});
// fetch cached data
caches.match('/data.json').then(function(response) {
if (!response) throw Error("No data");
return response.json();
}).then(function(data) {
// don't overwrite newer network data
if (!networkDataReceived) {
updatePage(data);
}
}).catch(function() {
// we didn't get cached data, the network is our last hope:
return networkUpdate;
}).catch(showErrorMessage).then(stopSpinner);
29. PWA Caching Strategies – Gabriele Falasca
RESPONDING TO REQUESTS
cache, then network
// IN THE SERVICE WORKER
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open('mysite-dynamic').then(function(cache) {
return fetch(event.request).then(function(response) {
cache.put(event.request, response.clone());
return response;
});
})
);
});
Ideal for: Content that updates frequently. E.g. articles, social media timelines, game leaderboards.
30. PWA Caching Strategies – Gabriele Falasca
RESPONDING TO REQUESTS
Other strategies
Cache and Network race
Generic fallback
Let’s start with a Breaking news, last week Google announced that PWA can be distributed through Play Store!!!
progressive web apps are a hybrid of regular web pages (or websites) and a mobile application; is a webapplication that you can install on your mobile device like a native app
You know it? :D today offline state is a problem for applications, today let’s analyze various use cases for managing user offline status
For migrating an existing web application to Progressive web app you have to do the follow: make sure your website runs under https, otherwise yoru application doesn’t work, you have to think about caching, that is the object of this talk, you have to elaborate a strategiy for the best compromise for managing request, you have to choose cache versus network for punctual situations, you have to develop an «add to homescreen» button for mobile visualization, after that you have to use the apis you like in the serviceworker, and then you can enjoy with your webapp
Before start i want to recap with you the difference between Server side Rendering and Client side rendering, you know?
In a classic server side rendering scenario the server returns a complete static page, and after that the client make requests for static sassets; every action on the page causes the complete reload of the page
Instead, in a client side rendering, the server return a blank page, or a blank template, and after the client fetch the resources and the dynamic content and build the page; in this case every action in the page causes only the reload of the section where action is triggered
If you want to cache things in a SSR scenario you can cache only assets and static page, and you can handle offline state with Javascript
If you want to cache things in a CSR scenario you can cache assets, static pages, and data using the cache api or indexedDB in case of complex data stractures, and you can handle offline state with Javascript
If you want to manage the offline state with Javascript there are two ways: a check to the navigator.offline flag or other solution event based
Do you know what is a Service worker?
Service workers essentially act as proxy servers that sit between web applications, the browser, and the network (when available). They are used things, to enable the creation of effective offline experiences, intercept network requests and take appropriate action based on whether the network is available, and update assets residing on the server. They will also allow access to push notifications and background sync APIs.
A best practice for create a real offline experience is to use the App shell model; in this model you have to separate the App Shell from the content, App Shell is the scaffolding part of the application, header, menu main content space ecc.. The content is the dynamic data fetched from the network. In this model is easy to cache all the «static» part of the application treating the dynamic content separately.
Okay, let’s see the use cases of this strategies in depth, starting to talk about when to cache resources
First strategy for caching things is to cache it on Service Worker «install» event, this is ideal for caching dependencies and static assets, but not only, you can cache also data from network that is really static, for example a list that not change in the time
Once a new ServiceWorker has installed & a previous version isn't being used, the new one activates, and you get an activate event. Because the old version is “deprecated”, it's a good time to handle schema migrations in IndexedDB and also delete unused caches.
Ideal for give the user a "Read later" or "Save for offline" button. When it's clicked, fetch what you need from the network & pop it in the cache. As you can see the cache is available from pages as well as service workers, meaning you don't need to involve the service worker to add things to the cache.
If a request doesn't match anything in the cache, get it from the network, send it to the page & add it to the cache at the same time. Be carefully using this strategies, because if the resource to fetch is too big there is a moment when you have a double copy in memory
I don’t think i’ve to illustrate what push notification is, i want say only that they are supported out of the box from Service Workers. Thanks to it, we can make requests also when the app is closed, when arrives a push notification,if we can make request in this scenario, we can also cache the response of the request
Another cool feature of service workers is the Background sync API (but be carefull, that is not standard API at today), is an api to manage the background sync of the data, also here, when is started the sync you can make requests and you can cache the response
If there's a cached version available, use it, but fetch an update for next time. For example Whatsapp or Telegram Avatar, if an user change his profile photo, other users continue to see the old for a bunch of time, until the new photo is loaded.
Now let's see what to do when the application made a network request, let’s see various use cases for choose cached data or network data, when is best to use one or anoter, and some hybrid approaches
This is the simplest approach; as you can see, in Service Worker we can intercept the «fetch» event, trigger every time an app starts a request, in this case we simply return the request whitout do anything, this is the only approach for non-GET request, form submission, generic POST, PUT or DELETE call.
Instead, this is the most used approach for the majority of generic GET requests, other patterns will be exeptions based on the incoming request, later let’s see a real Service Worker. This approach consists in check if exists a cached copy of that request, if not made the request on the network
This is the old approach of the majority of social networks, I made the fetch request and show to the online user the last updates, but offilne users can see ever the cached version of their feed
The newest approach of the social networks is this, i immediately show to the user the cached version of the data, then i made the network request and update the data on user feed, if the request fails there is no problem for the user
The code in slide before is the part that you can write directly in the application, this is the part of the code that you have to write in the Service Worker
There are other minor strategies for caching, Cache and Network race is an extreme use case, you can use this approach only if you are sure that the devices when the app will run have an extremely slow disk access, for using it you simply start the cache request and network request at the same time, and serve to the user the results obtained from the winner of this race, Generic Fallback is the classical page of error when is a problem :)