You may have noticed major updates on the Clever Cloud Console these last weeks. Along with the quick search feature, keyboard shortcuts and a new UI, we introduced the use of service workers to reduce the load time of the Console.
Service workers you said? Surely you mean Web Workers, right?
No! Service workers are scripts which are run by your browser, in the background. They are completely separated from the web pages of your site and have multiple uses. You can use them for HTTPS proxying, push notifications, or even background syncing.
Today, we will cover HTTPS proxying.
The Console should load faster
When you load the Clever Cloud Console, we need to fetch data (the user, its organisations / apps / addons…) to bootstrap the whole application. If you have a lot of applications / belong to a lot of organisations, this may take a while, up to several seconds ("a lot" can mean ~200 apps for some of ourusers). This data isn't updated a lot. Most of our customers are using the Console to look at the logs or configure their applications. So it needs to be fast for them to access / set the information they want.
Here come the service workers. They can cache the response of any HTTPS request that is emitted from your application (which can be used to build offline apps). What we did is that any huge XHR request done when the Console is loading is now served from the cache if it exists.
How does it work?
- These new APIs (Service Workers, Cache API, Fetch API) are heavily using ES6 Promises, you should be familiar with them
- Incidentally, you can use ES6 because browsers that support service-workers are also implementing mainstream ES6 features
- You need to use a recent browser: Chrome 42+, Firefox 44+
- Service Workers are HTTPS only, they won't work on HTTP requests. Chrome will allow http://localhost as a secure origin though.
How to debug
- You can check "Enable Service Workers over HTTP (when toolbox is open)" on Firefox Devtools to use service workers on any HTTP request.
- Chrome will only allow localhost or HTTPS urls. Good thing that HTTPS is available on *.cleverapps.io.
- Firefox and Chrome have an endpoint to list service workers: Firefox:
about:serviceworkers
, Chrome:chrome://inspect/#service-workers
- You can use regular
console.log()
to log from the service worker. - You won't be able to see intercepted requests in the Network panel of Firefox (as of Firefox 45.0a1).
The code
First, we need to setup our service worker and ask the browser to register it (in your existing javascript application):
navigator
.serviceWorker
.register('/service-worker.js')
.then(navigator.serviceWorker.ready)
.then(registration => {
console.log('Server worker has been registered');
});
You don't have to worry about multiple registrations: if a service worker has already been registered, the browser will simply ignore it.
Once this is done, we have to create a service-worker.js file. Service Workers are event based. Two events will be useful in our case: install
and fetch
. install
will allow us to initialize our service worker (like pre-caching assets we haven't already loaded). fetch
will be triggered each time a request is intercepted for the current scope.
We will use the Cache API to cache requests / responses.
'use strict';
const CACHE_URLS = [
"/img/img1.png",
"/img/img2.png",
"/users/me"
];
const CACHE_NAME = "cc-cache-test";
self.addEventListener('install', event => {
event.waitUntil(
caches
.open(CACHE_NAME)
.then(cache =>{
cache.addAll(CACHE_URLS);
})
.catch(e => console.error("Couldn't install the service worker, reason:", e));
);
});
self.addEventListener("fetch", event => {});
Here, during the installation step, the service worker will download and cache the CACHE_URLS urls even before the user requests them. Now, we need to tell our SW to use the cache whenever we have a cached version of the request or to fetch it if we don't. We can use the great HTML5 Fetch API:
self.addEventListener("fetch", event => {
event.respondWith(
caches.open(CACHE_NAME)
.then(cache => cache.match(event.request))
.then(response => {
if(response){
return Promise.resolve(response);
} else{
return fetch(event.request).then(res => {
if(res && res.status === 200){
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, res);
});
}
return res.clone();
});
}
})
);
});
What we are doing is to open the cache and then try to match the current request. If we have a match, we return the response associated to the request. If we don't, we fetch the request, check its response code and save it into the cache if it succeeded. Next time, it will load faster!
Note that we have to use res.clone();
. This is because the response is a stream which can be read only once. Here, we want to store its data into the cache AND return the same data to the browser. This won't work unless we .clone()
it first.
Now, our service worker is ready to be used.
Sounds perfect but…
A major drawback is that you need a quite recent browser and these API may differ between browsers. At the time of writing, this won't work on Safari (any version), Firefox stable (aurora/dev has some support), Opera, Internet Explorer (Edge does support some features). You will have to handle these incompatibilities yourself.
But, even if service workers are not yet supported by all browsers, your application will still be able to run in a normal way. Only proxying and cache features will be affected, requests will be done as they were before. This is completely backwards compatible.
How do we use it at Clever Cloud
We can't store all of the data or cache invalidation will be a mess to handle. Instead, we only store a few defined requests. When we load the Console, we use the cache, then wipe and update it in the background. The user can use the Console with quite fresh data and the UI will be updated as soon as we have the new one.
This feature allowed us to speed up the initial load of the Console, going well under 1 second if the cache exists. In a near future, we could also use service workers with the Notifications API to display application deployments's state even when the Console is not open in your browser.
More reading
To learn about service workers, I used a few links:
- Service Workers
- Cache API
- ES6 Promises
- HTML5 rocks (may be a little out of date but still accurate and well explained)