Using web workers on Angular 6

Daniel Amores
7 min readSep 10, 2018

--

Long story short

Let’s say you’re working on some front end application at your work.
Bill has been in charge of the back end development and he has decided that the application APIs will pretty much export everything existing on the database as is, so that consumers can make the most out of the application data — let’s not get into how right or wrong this may be just to keep the article on it’s scope.

Bill’s decission means you’ll be performing loads of data transformations to adapt the API. All these maps and reduces will most likely impact how quick the application renders and they may even block the user interface if the data volume becomes too big at some point*. How can you perform all the required transformations while keeping the user interface performant?

Web workers to the rescue.

* JavaScript is a single-threaded language, meaning that all your operations are run by the very same thread. If you perform compute intensive tasks on the thread, the rest of the tasks will have to way for those to be finished, leading to sloppy UI rendering and whatnot.

What is a web worker? How is it gonna help me?

A web worker, as defined by the W3C, is a JavaScript running on the background independently of user-interface scripts. This basically means they’ll provide an execution context isolated from the user interface on which we can run anything we want.

Now, how does a web worker look like?
In the following example we’ll use a web worker to calculate factorials. We won’t be using any framework as of yet, but plain JavaScript and HTML.

First, we’ll create an script containing the web worker definition and logic:

function factorial(number) {
function recFact(number, result) {
if (number === 1 || number === 0) {
return result;
} else {
return recFact(number - 1, number * result);
}
}

return recFact(number, 1);
}
self.addEventListener('message', function(event) {
var results = [];
var limit = event.data.limit;
for (var num = 1; num <= limit; num++) {
results.push({ number: num, result: factorial(num) });
}
postMessage(results);
});

Web workers are able to send data back to their creator by using postMessage.
We can also add event listeners within the web worker script so that it can be executed only under certain circunstances rather than immediately.

Then, we’ll use it on a very simple html page:

<!DOCTYPE html>
<html>
<head>
<title>Worker example</title>
</head>
<body>
<input type="number" id="limit" value="50">
<button onclick="executeWorker()">Execute worker</button>
<div id="results">
</div>
<script>
var worker = new Worker('worker.js');
worker.onmessage = function (event) {
var resultsDiv = document.getElementById('results');

for (var resultIdx = 0; resultIdx < event.data.length; resultIdx++) {
var result = event.data[resultIdx];
var node = document.createElement('p');
var textNode = document.createTextNode('Factorial of ' + result.number + ' is ' + result.result);
node.appendChild(textNode);
resultsDiv.appendChild(node);
}
};

function clearResults() {
var resultsDiv = document.getElementById('results');
while (resultsDiv.firstChild) {
resultsDiv.removeChild(resultsDiv.firstChild);
}
}

function executeWorker() {
clearResults();
var limit = document.getElementById('limit').value;
worker.postMessage({ limit: limit });
}
</script>
</body>
</html>

We use Worker to create a new worker instance that references our web worker script. Then we bind attach a function to onmessage event handler so that we receive and process messages coming from the web worker.

Figure 1: Plain JavaScript and HTML worker

Ok, I understand what is a web worker. How do I use one on Angular?

Now this was quite an interesting travel for me. I tried different options, from having a separated Webpack build for the web worker scripts to using a web worker loader on the main build. I was unable to make yield the results I expected, so I ended up using a solution based on ngx-web-worker, even thought I made very slight changes.

So, ngx-web-worker will take a function as input parameter, transform it into a a Blob, execute it on a web worker and resolve a Promise we can easily subscribe to. Simple, right?
Keep in mind that in order to use external libraries or scripts we have to use importScripts within our worker at run time; this means that we’ll have to extract those external libraries or scripts on our Angular application bundle.

Great, let’s see an example!

For our example, we’ll be implementing a very simple Angular application that features two examples.

For the first example, we’ll be calculating factorials with a large added overhead — a while loop with a lot of iterations — just to prove how web workers improve the user interface performance by offloading heavy tasks.

For the second example, we’ll be exporting a data table on an Excel file just to prove how to include external scripts within our web worker script.

While we’ll take a look at the most important parts of the example on this article, you can find all the working source code on this Github repository.

Figure 2: Angular with web workers!

First of all, here’s the web worker service, slightly modified from the original ngx-web-worker version by removing the postMessage call from the web worker string skeleton at line 114.

The service basically wraps the web worker code on a Blob and runs it. This simplifies a lot the process of packaging, bundling and distributing the worker scripts.
It also wraps responses on Promises so that it’s very easy to subscribe and process web workers responses once they’re ready.

Once we’ve taking a quick glance at the web worker service, it’s time we dive into the scripts that will be run by it. We’ll begin by taking a look at our factorial calculation web worker script:

We use a recursive function to calculate the factorial of the given number, there’s nothing really shiny or special here. Note the loop between lines 31 and 34, it’s purpose is to slow the calculations down in order to better proove how the web worker affects performance.
You can also see how postMessage is used to return an answer to the web worker invoker. Last but not least, since this script will be run both in and out of a web worker, we need to adapt the return accordingly.

Now, we’ll take a look at our Excel export web worker script.

This script uses xlsx to create a binary Excel book and returns it. importScripts is used to allocate the external xslx script and postMessage is used to return the process result to the invoker.

However, in order to be able to find the script at the desired location, we must bundle it with our application. To achieve this, we’ll be using angular.json configuration since our project was created with Angular CLI, in case you’re not using the CLI you can just use Webpack Copy Plugin.

On the following code snippet you can see an example of how to configure the CLI in order to export library scripts together with the application bundle.

Finally, all we have to do in order to use these scripts in our Angular components is inject the web worker service and invoke it as show on the snippet below:

Keep in mind we’re forwarding the window Location data to the worker in case it has to import any external library or script.

How does it affect performance?

So now that we’ve seen how to implement web worker scripts and how to run them using our Angular service, it’s time to see how they affect performance.

In order to do this, we’ll be using the factorial calculation example since it features both a web worker and a non web worker execution. We’ll be using Mozilla Firefox’ performance recorder to track which events happen on each case.

So first, we’ll be running the non web worker example:

Figure 3: Factorial without web worker

FACTORIAL_SCRIPT takes up to 87% of the compute time, effectively blocking the user interface rendering and possible event handlers or callbacks — this could be critical in the case of real time applications that use web sockets and such.

Now let’s take a look at the very same execution using a web worker:

Figure 4: Factorial with web worker

In this case, all the heavy lifting was performed on the web worker, isolated from the main thread, and no user interface process was blocked during the execution. This is extremely important and you can benefit greatly from it on applications that target mobile devices.

Now if we take a look at execution times we’ll find out that they are pretty much the same. This is quite obvious since the calculations performed is the same, they’re just offloaded to a web worker in order to keep our ui thread as clear as possible.

Figure 3: Execution time comparison

Important notes

An issue I ran into is that web worker typings are not easily imported. I found out that the easiest way is to redeclare the specific functions — usually importScripts and postMessage — we use in our code over trying to import the webworker lib on the tsconfig file (source).

It is also worth noting that external libraries or scripts to be imported using importScripts must be exported as assets. Remember you can configure this at the angular.json file or simply using Webpack copy plugin in case you’re not using the CLI.

Conclusions

While implementing a web worker on Angular was not a really straightforward path to me, it was definitely not that hard and they’re a great tool to improve front end performance, specially for mobile first applications that target devices not as powerful as gool old home computers.

Feel free to share your thoughts or drop me a line at the comments!

--

--