Angular Performance: Web Workers

Learn about Web Workers and how to use them in your Angular app.

Chidume Nnamdi 🔥💻🎵🎮
Bits and Pieces

--

Web apps would run better if heavy computations could be performed in the background, rather than compete with the user interface.

We need all apps to be very fast and highly responsive. Like lightning fast and responsive. So in that way, we have researched and found many ways of making our apps very fast, to keep our users. It is said humans are very impatient wait for an app to load or run some computations, we will leave a site if it takes ~3s to run/load. So check your analytics you may have a wonderful website but may have low visitors because of how performant(low or high) your app is, you need to check it today. In this post, we will discuss one of the optimization tricks to make our Angular apps highly performant.

Before we start: Use Bit to encapsulate Angular components with all their dependencies and setup. Build truly modular applications with better code reuse, simpler maintenance and less overhead.

Share and collaborate on individual Angular components.

Web Worker in general

Web Worker is really a worker, working its ass off because of the heavy load it is given. In computing context, Web Worker is a background thread that is created alongside/parallel the main thread where heavy computations are done to prevent drag on the main thread.

Web worker is one of the many workers we have in web. There are

All do the same thing, creating a background thread but run different functions. So the similar thing they do is create a parallel thread.

What is a thread? Whenever a program runs, it is allocated a space(memory address space) in RAM where its code and data are stored and executed by the CPU instruction by instruction one at a time. The memory space allocated is called the main thread. A thread is a piece of code that has its codespace inside a process. If a CPU supports multi-threading, it can run multiple threads at a time.

What is multi-threading? This is the concurrent execution/running of threads at the same time in a process. In Win, we create threads using the CreateThreadEx(...) API. This creates a codespace parallel to the main thread. The CPU executes both threads by executing one for a time saves its context, load the next thread in the registers and continues with its execution. This context switch happens on a scale of ~billionth(1/10^9) of a second that it appears to us to happen at the same time and very fast.

So in Worker, the JS engine creates a new thread and loads the JS script in the thread. In multi-threading, threads communicate with each other using shared resources. The shared resource is placed in a central place in the RAM where the threads can read/write to the resource thereby communicating with each other.

In the browser, we have the main thread which is called the DOM thread, during the loading of a new script in a tab, a DOM thread is created where the JS engine loads, parses and executes the JS files in the page. The Worker will create a new thread called the Worker Thread that will run a JS script parallel to the DOM thread. The JS script run by the Worker thread would not have a reference to the DOM because it is running in a different environment where no DOM APIs exists.

Using Web Worker

Like we said earlier, Workers don’t have access to the DOM APIs. They can’t access any of the following:

  • window object
  • document object

Web Workers can access:

  • navigator object
  • location object (read-only)
  • XMLHttpRequest
  • setTimeout(), clearTimeout(), setInterval(), clearInterval()
  • The atob() and btoa() functions

Web Workers can also access

  • Cache object

Web workers can run asynchronously and synchronously. Web Workers run their code from top to down then they enter into event loop, executing tasks/callbacks scheduled by event (APIs like setTimeout, setInterval, etc). First, they run synchronously executing the script then they enter asynchronous listening for events and executing them.

Web Worker provides methods/APIs that we can use to run our JS script. To instantiate a Worker, we run this:

const webWorker = new Worker('./script.js')

The JS script file is passed to the Worker when instantiating it. The Worker will create a new thread, parse and generate machine code from the JS script file, the machine code will be loaded in the new webWorker thread memory address space. Then the CPU will concurrently run the DOM thread and the webWorker thread.

Web Worker can send messages to the DOM thread that spawned it. It has several event listeners that trigger a registered callback when the event is fired.

onmessage

This is triggered when a message is received.

webWorker.onmessage = function(event) {
// ...
}

onerror

This is triggered when an error is thrown in the Worker.

webWorker.onerror = function(event) {
// ...
}

We can send messages using the postMessage API

From the DOM to the Web Worker

webWorker.postMessage(data)

From the Web Worker to the DOM thread:

postMessage(data)

We can listen for events using the addEventListener API

webWorker.addEventListener('message', function(evt) {
// ...
})

Using in Angular

It seems Web workers are used in vanilla JavaScript apps, most of us are addicted to Angular. We, Angular developers, have tried to use Web Workers in Angular but eh Webpack configuration and all proved very painful to setup. Up until the release of Angular CLI v8, using Web Workers in Angular, its setup, compiling, bundling and code-splitting was made easy by the CLI tool.

To generate a Web Worker, we run the ng g web-worker command:

ng g web-worker webworker

This will generate webworker.ts file in the src/app of an Angular app. The web-worker tells the CLI tools that the file would be used by a Worker.

To demonstrate how to use Web worker in Angular to optimize its performance. Let’s say we have an app that calculates Fibonacci numbers. Finding Fibonacci numbers in the DOM thread will kinda impact the UI experience because the DOM and the user interactions would freeze until the number is found.

Starting, our app would be like this:

// webWorker-demo/src/app/app.component.ts@Component({
selector: 'app',
template: `
<div>
<input type="number" [(ngModel)]="number" placeholder="Enter any number" />
<button (click)="calcFib">Calc. Fib</button>
</div>
<div>{{output}}</div>
`
})
export class App {
private number
private output
calcFib() {
this.output =fibonacci(this.number)
}
}
function fibonacci(num) {
if (num == 1 || num == 2) {
return 1
}
return fibonacci(num - 1) + fibonacci(num - 2)
}

Calculating Fibonacci numbers is recursive, passing small numbers like 0–900 would have no performance impact. Imagine passing ~10,000. That’s when we will begin to notice performance drag. Like we said the best bet is to move the fibonacci function or algorithm to execute in another thread. So no matter how large the number is, it will not be felt in the DOM thread.

So we scaffold a Web Worker file:

ng g web-worker webWorker

and move the fibonacci function into the file:

// webWorker-demo/src/app/webWorker.ts
function fibonacci(num) {
if (num == 1 || num == 2) {
return 1
}
return fibonacci(num - 1) + fibonacci(num - 2)
}
self.addEventListener('message', (evt) => {
const num = evt.data
postMessage(fibonacci(num))
})

Now we will edit the app.component.ts to add Web Worker

// webWorker-demo/arc/app/app.component.ts@Component({
selector: 'app',
template: `
<div>
<input type="number" [(ngModel)]="number" placeholder="Enter any number" />
<button (click)="calcFib">Calc. Fib</button>
</div>
<div>{{output}}</div>
`
})
export class App implements OnInit{
private number
private output
private webworker: Worker
ngOnInit() {
if(typeof Worker !== 'undefined') {
this.webWorker = new Worker('./webWorker')
this.webWorker.onmessage = function(data) {
this.output = data
}
}
}
calcFib() {
this.webWorker.postMessage(this.number)
}
}

Our code is now 😘. We added ngOnInit lifecycle hook in our component so to initialize the Web Worker with the Web Worker file we generated earlier. We registered to listen to messages sent fro the Web Worker in the onmessage handler any data we get we will display it in the DOM.

We made the calcFib function to send the number to Web Worker. This below in webWorker would capture the number

self.addEventListener('message', (evt) => {
const num = evt.data
postMessage(fibonacci(num))
})

and processes the Fibonacci number then send the result back to the DOM thread. The onmessage we set up in the app.component

ngOnInit() {
if(typeof Worker !== 'undefined') {
this.webWorker = new Worker('./webWorker')
this.webWorker.onmessage = function(data) {
this.output = data
}
}
}

would receive the result in data then we will display the result in the DOM using the {{output}} interpolation.

During the processing of the Fibonacci numbers, the DOM thread would be left focusing on the user interactions while the webWorker would do the heavy processing.

Another Example: Prime Number

Let’s have a look at a simple example that calculates prime numbers:

// primes-demo/src/app/app.component.ts@Component({
selector: 'app',
template: `
<div>
<input type="number" [(ngModel)]="number" placeholder="Enter any number" />
<button (click)="calcPrimes">Calc. Prime Numbers</button>
</div>
<div>{{output}}</div>
`
})
export class App {
private number
private output
calcPrimes() {
this.output = this.getPrimes(this.number)
}
getPrimes(number) {
if (typeof number == "number") {
if(number<0){
return "negative integers can not be prime";
}
if(number==0){
return "zero is not a prime number";
}
if(number==1){
return "1 is not a prime number";
}
var nonprimes = [], // Array of non prime numbers
var i,j,primes = []; // Array of prime numbers
for (i = 2; i <= number; ++i) {
if (!nonprimes[i]) {
// i has not been marked -- it is prime
primes.push(i);
for (j = i << 1; j <= number; j += i) {
nonprimes[j] = true;
}
}
}
return primes; // Array of prime numbers
}
else{
return "invalid input";
}
}
}

Number entered in the input box is held in the number property, when the Calc. Prime Numbers button is clicked the calcPrimes method is called which calls the getPrimes function, this contains the algorithm to calculate prime numbers of a number, this function returns an array that contains the number primes, then the calcPrime assigns it to the output property which is displayed on the browser.

Prime number calculation can become expensive when the number increases, so prime number calculation is a good candidate to be offloaded to the Web Worker thread.

So we scaffold a new Web Worker file:

ng g web-worker primesWorker

Then we add the code to primesWorker:

// primes-demo/src/app/primesWorker.tsfunction getPrimes(number) {
if (typeof number == "number") {
if(number<0){
return "negative integers can not be prime";
}
if(number==0){
return "zero is not a prime number";
}
if(number==1){
return "1 is not a prime number";
}
var nonprimes = [], // Array of non prime numbers
var i,j,primes = []; // Array of prime numbers
for (i = 2; i <= number; ++i) {
if (!nonprimes[i]) {
// i has not been marked -- it is prime
primes.push(i);
for (j = i << 1; j <= number; j += i) {
nonprimes[j] = true;
}
}
}
return primes; // Array of prime numbers
}
else{
return "invalid input";
}
}
self.addEventListener('message', (evt) => {
const num = evt.data
postMessage(getPrimes(num))
})

Then our app.component.ts will be rewritten to this:

// primes-demo/src/app/app.component.ts@Component({
selector: 'app',
template: `
<div>
<input type="number" [(ngModel)]="number" placeholder="Enter any number" />
<button (click)="calcPrimes">Calc. Prime Numbers</button>
</div>
<div>{{output}}</div>
`
})
export class App implements OnInit {
private number
private output
private primesWorker: Worker
ngOnInit() {
if(typeof Worker !== 'undefined') {
this.primesWorker = new Worker('./primesWorker')
this.primesWorker.onmessage = function(data) {
this.output = data
}
}
}
calcPrimes() {
this.primesWorker.postMessage(this.number)
}
}

The prime number calculation is now being done in another thread leaving the DOM thread free.

The thing here is that the time it would take a heavy calculation to complete in the main thread is the same as in the Worker thread. Moving it off to the Worker thread doesn’t reduce the calculation speed, it just prevents the DOM thread from locking and becoming unresponsive.

Terminating a worker

According to Using Web Worker APIs — Wikipedia

If you need to immediately terminate a running worker from the main thread, you can do so by calling the worker’s terminate method:

webWorker.terminate();

The worker thread is killed immediately.

In our Angular example, we didn’t kill the Worker thread. When the app.component.ts is destroyed the Worker thread will still be left open and be hanging around. This is very bad practice, we should clean up the Worker thread when we are done with it.

To do so, we will utilize the ngOnDestroy hook in Angular. This hook is what Angular calls when a component is being destroyed, so we will terminate the Worker thread there.

// primes-demo/src/app/app.component.ts@Component({
selector: 'app',
template: `
<div>
<input type="number" [(ngModel)]="number" placeholder="Enter any number" />
<button (click)="calcPrimes">Calc. Prime Numbers</button>
</div>
<div>{{output}}</div>
`
})
export class App implements OnInit, OnDestroy {
private number
private output
private primesWorker: Worker
ngOnInit() {
if(typeof Worker !== 'undefined') {
this.primesWorker = new Worker('./primesWorker')
this.primesWorker.onmessage = function(data) {
this.output = data
}
}
}
calcPrimes() {
this.primesWorker.postMessage(this.number)
}
ngOnDestroy() {
his.primesWorker.terminate()
}
}

In the fibonacci example:

// webWorker-demo/arc/app/app.component.ts@Component({
selector: 'app',
template: `
<div>
<input type="number" [(ngModel)]="number" placeholder="Enter any number" />
<button (click)="calcFib">Calc. Fib</button>
</div>
<div>{{output}}</div>
`
})
export class App implements OnInit, OnDestroy {
private number
private output
private webworker: Worker
ngOnInit() {
if(typeof Worker !== 'undefined') {
this.webWorker = new Worker('./webWorker')
this.webWorker.onmessage = function(data) {
this.output = data
}
}
}
calcFib() {
this.webWorker.postMessage(this.number)
}
ngOnDestroy() {
this.webWorker.terminate()
}
}

Conclusion

In this post, we saw what Web Worker is, its APIS and how to use it. Further down, we saw how to use the Angular CLI to easily add Web Worker to our Angular apps and we saw its usage in Angular on how we moved our Fibonacci calculation to the Worker thread.

In as much Worker is good in offloading our work to the Worker thread, we should also try to write optimizable code in the Worker script because it would take the same time in the main thread. So adding Worker to a good JS code would be lit, your app would be blazingly fast 🚀.

If you have any question regarding this or anything I should add, correct or remove, feel free to comment, email or DM me.

Thanks !!!

Related Stories

Encapsulates components with Bit to run them anywhere across your projects and applications

Bit encapsulates components in your projects with all their files and dependencies, so they can run anywhere across your applications.

Build faster by making your components reusable out-of-the-box, and collaborate as a team to share and discover components. No refactoring or configurations needed, just share components and build truly modular apps.

LEARN MORE

--

--

JS | Blockchain dev | Author of “Understanding JavaScript” and “Array Methods in JavaScript” - https://app.gumroad.com/chidumennamdi 📕