Performance benefits of using Service Worker in Angular app
By: Kamil Maraz
Tags: Angular, Ngrx, Performance, Service Worker, Caching

We all strive to deliver the best performance of our apps. So, how would you do it? You can reduce bundle size and split bundle into multiple chunks. Then lazy load and cache everything. You can do this or that but after everything is done perfectly, what should be the next step?

You guessed right: Service Workers.

In this article, you will learn how to set up Service Worker using Workbox with custom Webpack configuration. For state management, let’s use NgRx. But anything will do. You can be as hardcore as you want. This setup will improve bootstrap of our application by caching assets and, most importantly, GET requests.

In this example, our guinea-pig will be Slido Admin mentioned in previous articles.

So, let’s begin

As usual, the first step in our journey is:

npm install workbox-webpack-plugin workbox-window @types/workbox-window --save

The next step is to configure Webpack. It is essential to configure SSL properly. Service Worker needs valid SSL certificate to be able to run in a browser.

Now you should configure Service Worker file.

The Service Worker lifecycle consists of 5 phases. parsed, installing, installed (waiting), activating, activated. When Service Worker is parsed, it tries to install itself. When it is installed, the skipWaiting command will force it to activate itself immediately.

“… you can use skipWaiting and clientsClaim to force the new service worker to immediately take control of open pages.¹”

If you want to understand it in more depth, read the official documentation here and here.

Now is the time to install Service Worker. Depending on your application, the best place should be somewhere during its bootstrap phase. Preferably in Injectable .

In Admin, we need to make sure that both AngularJs and Angular parts are loaded before doing any Service Worker magic. This article about Angular hybrid architecture will give you more detail.

You can set it up like this:

And then write WorkerService Injectable:

In the snippet above the Workbox is imported dynamically (a matter of taste). Provided that everything went according to plan, your Service Worker is now installed and running.

What about performance goals?

The first thing that comes to mind is cache. Let’s do that straight away.

For caching assets, you need to register a route. Then pick a caching strategy. For the purpose of this example, we are using Cache first strategy.

“Offline web apps will rely heavily on the cache, but for assets that are non-critical and can be gradually cached, a cache first is the best option.²”

Expiration plugin sets the life of cache and manages its resetting when memory quota is reached.

The next caching strategy in this example is called State while revalidate. Let’s demonstrate it by caching Google Fonts used in Admin application.

“The stale-while-revalidate pattern allows you to respond to the request as quickly as possible with a cached response if available, falling back to the network request if it’s not cached. The network request is then used to update the cache.³”

You should use it for all the data which can be updated more often. However, you still want to benefit from speed of local cache.

Simple as that.

Those were quick wins. Now is the time for magic.

Imagine that during bootstrap, your application is doing a few GET requests.

For example:

1. request: GET https://api.example.com/users

2. request: GET https://api.example.com/accounts

3. request: GET https://api.example.com/things

Now, let’s suppose we identified the following:

Requests for users and accounts have very small payload and change only once in a while

and have very small payload and change only once in a while Request for things is often very heavy and takes very long time to download but could potentially change frequently

The first two endpoints are clearly the case for caching responses. Depending on the update frequency of things endpoint, it might be a good idea to cache the responses locally, too.

The next time this request is made, it will also be cached by Service Worker. Easy, right? Cached response will be used every time GET request is made.

What if the response has changed? What is the best way to notify your application and reflect this change in UI?

To communicate with your application, use the Broadcast update plugin. channelName is self-explanatory. headersToCheck is an array of headers for comparing request response and cached values. If there is a difference, the plugin will replace cached response and notify the application. Checking headers is way faster than comparing the whole response body.

Remember how you initialized Service Worker? The next step is to listen to it.

First, listen to BroadcastChannel initialized by broadcastUpdate plugin in Service Worker. Message handler will notify your application about cache updates. Then, you should collect all the changes from the browser caches .

Next, send it to your application. The nice thing about NgRx is that you can reuse the same Effects as you use for your standard API call processing.

Here is an example of how ThingsEffects can look:

As you can see in the snippet above, after fetching things the update action is called. You can call this action also from method dispatchData in WorkerService. That way you achieve state (and UI) update easily.

Quick tip #1

You can use Logout action or actions fired when the user is not authorized to clear browser cache. The same as you do with tokens and cookies.

Quick tip #2

Sometimes during the bootstrap of a hybrid application (NgUpgrade), resources can be loaded in AngularJs service before Injectables are initialized.

You can use a temporary solution which includes storing updated data in a global variable (I know, I know…) and using it in Injectable later.

Here is the very top of app.module.ts:

app.module.ts

The update to the initialize function looks like this:

There are many ways how this could go wrong for the users. Make sure you implement everything mentioned in this article carefully and test it properly.

A real-world example

Everything until this point was a source code sitting in the comfort of your localhost. Now you are going test it in the wilderness.

The aim for this article is to help you improve bootstrap time. Slido Admin is pretty heavy, so we saw improvements immediately.

We measured load times without and with the Service Worker enabled. Also, we simulated network speeds used by our clients.

Here are some numbers:

In this case, the bootstrap time decreased significantly. The improvements can be even bigger in applications that are more network heavy.

Wrap up

As you can see, setting up a Service Worker in Angular application is easy. Even when it needs to be done manually and not by using Angular CLI. You can easily manage perceived bootstrap time by caching data. Even network data.

If this is the case, then Service Workers play on your side. The performance benefits of using this approach can be minuscule or huge. It depends on your use case. So, remember to do performance tests after every step mentioned in this article. You never know.

Sources

[1]: https://developers.google.com/web/tools/workbox/guides/generate-complete-sw#skip_waiting_and_clients_claim

[2]: https://developers.google.com/web/tools/workbox/modules/workbox-strategies

[3]: https://developers.google.com/web/tools/workbox/modules/workbox-strategies