Skip to content
Angular Performance Optimization - Web Workers

Angular Performance Optimization - Web Workers

In a previous blog post we identified a potential performance issue caused by calling a method from the template of an Angular component and saw that time-consuming operations could still be a problem even for an optimized application.

Prime Number Application

Using the same use case - a very simple application consisting of a number input that compute the next prime number -, we will now study how to externalise long-running operations to a web worker.

Web workers makes it possible to run a script operation in a background thread separate from the main execution thread of a web application. The advantage of this is that laborious processing can be performed in a separate thread, allowing the main (usually the UI) thread to run without being blocked/slowed down.

Do YOU have custom Load Testing needs?
Rely on our Expertise

Generating a Web-Worker with Angular

Angular CLI provides a very convenient way to generate a usable web-worker for an existing application.

The following command generates a file named prime.worker.ts:

ng generate web-worker prime

Its content is simple, this web worker only returns the given data as a response:

/// <reference lib="webworker" />

addEventListener('message', ({ data }) => {
  const response = `worker response to ${data}`;
  postMessage(response);
});

The Angular CLI command also generates the following code to call the web-worker:

if (typeof Worker !== 'undefined') {
  // Create a new
  const worker = new Worker(new URL('./app.worker', import.meta.url));
  worker.onmessage = ({ data }) => {
    console.log(`page got message: ${data}`);
  };
  worker.postMessage('hello');
} else {
  // Web Workers are not supported in this environment.
  // You should add a fallback so that your program still executes correctly.
}

The configuration file angular.json is updated and a tsconfig.worker.json file is generated. All you need to do is to restart the application and start using the created web-worker!

As it is, this web-worker does not help our prime-number computation problem. We need to extract the logic and use it in the generated web-worker.

Extracting the prime number logic

In the previous version of our application, we used a synchronous Angular service to compute:

  • isPrime: tells if the given number is prime,
  • nextPrime: compute the next prime number.
import {Injectable} from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class PrimeService {

  public isPrime(number: number): boolean {
    if (number <= 1) {
      return false;
    }
    for (let i = 2; i < number; i++) {
      if (number % i == 0) {
        return false;
      }
    }
    return true;
  }

  public nextPrime(number: number): number {
    while (!this.isPrime(++number)) {
    }
    return number;
  }
}

Both these operations can take some time as we use the most basic computational method: trial division.

The most basic method of checking the primality of a given integer n is called trial division. This method divides n by each integer from 2 up to the square root of n. Any such integer dividing n evenly establishes n as composite; otherwise it is prime.

We need to externalise this logic in a static Object, so we can call it from the previously generated web-worker (or directly from a service if web-workers are not available, for example when using Server-side rendering).

Let's create a file named prime-static.ts with the same methods as our PrimeService but here defined as static methods:

export type PrimeOperation = 'isPrime' | 'nextPrime';

export class PrimeStatic {
  public static isPrime(number: number): boolean {
    if (number <= 1) {
      return false;
    }
    for (let i = 2; i < number; i++) {
      if (number % i == 0) {
        return false;
      }
    }
    return true;
  }

  public static nextPrime(number: number): number {
    while (!PrimeStatic.isPrime(++number)) {
    }
    return number;
  }

  public static primeOperation(number: number, operation: PrimeOperation): number | boolean {
    switch (operation) {
      case 'isPrime':
        return PrimeStatic.isPrime(number);
      case "nextPrime":
        return PrimeStatic.nextPrime(number);
      default:
        throw 'Unknown operation';
    }
  }
}

The isPrime and nextPrime methods are similar to the previous version. We added the primeOperation method that can dynamically call the other two depending on the operation: PrimeOperation parameter.

This method is used in the updated prime-worker.ts web-worker:

/// <reference lib="webworker" />

import {PrimeOperation, PrimeStatic} from "./prime-static";

addEventListener('message', ({data}) => {
  const number: number = data.number;
  const operation: PrimeOperation = data.operation;
  postMessage(PrimeStatic.primeOperation(number, operation));
});
So, depending on the given data, this web-worker will either compute the next prime value or tell if the current one is a prime.

Now let's create another version of the PrimeService that uses this web-worker!

Using the Web-Worker in an async service

We call it PrimeAsyncService. This service being asynchronous, it returns Observables instead of raw values. These observables send a next value when the prime-worker web-worker has finished its task and posted the answering message.

The file prime-async.service.ts:

import {Injectable} from '@angular/core';
import {Observable, ReplaySubject} from "rxjs";
import {PrimeOperation, PrimeStatic} from "./prime-static";

@Injectable({
  providedIn: 'root'
})
export class PrimeAsyncService {

  public isPrime(number: number): Observable<boolean> {
    return this.callWorker({number, operation: 'isPrime'});
  }

  public nextPrime(number: number): Observable<number> {
    return this.callWorker({number, operation: 'nextPrime'});
  }

  private callWorker<R>(data: { number: number, operation: PrimeOperation }): Observable<R> {
    const subject = new ReplaySubject<R>(1);
    if (typeof Worker !== 'undefined') {
      // Create a new
      const worker = new Worker(new URL('./prime.worker', import.meta.url));
      worker.onmessage = ({data}) => {
        subject.next(data);
      };
      worker.postMessage(data);
    } else {
      // Web Workers are not supported in this environment.
      subject.next(PrimeStatic.primeOperation(data.number, data.operation) as unknown as R);
    }
    return subject;
  }

}

The callWorker() function is an update of the code generated by Angular to call the web-worker:

  1. It declares a subject,
  2. Checks if web-workers are available,
  3. Calls the prime worker if it is,
  4. Directly calls the PrimeStatic object otherwise,
  5. Returns the created subject.

When the prime worker sends a message, it sets the next value of the subject. The caller of the method can subscribe to it and be notified asynchronously with the operation result.

Stateful CallWorkerComponent

We can now create the CallWorkerComponent component that uses this service to display a number input form, a button to get the next prime number, and text that tells if the current value is a prime number.

Here is a screenshot of such component in loading the state:

Prime Number Application Loading

Two class fields are used in the HTML template for the component state: loading and isPrime. Remember from our previous blog post that methods should not be called from Angular template to avoid performance drawbacks.

<input type="number" [formControl]="numberForm">
<button type="button" (click)="computeNextPrimeNumber()" [disabled]="loading">{{loading ? 'Loading...' : 'Next'}}</button>
<p>is prime number ? {{isPrime}}</p>

We also use the OnPush change detection strategy to apply all tricks learned until now to maximize the performances of our application :

import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy} from '@angular/core';
import {mergeMap, Subject, takeUntil} from "rxjs";
import {FormControl} from "@angular/forms";
import {PrimeAsyncService} from "../prime-async.service";

@Component({
  selector: 'app-call-worker',
  templateUrl: './call-worker.component.html',
  styleUrls: ['./call-worker.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CallWorkerComponent implements OnDestroy {

  private readonly destroyed = new Subject<boolean>();
  readonly numberForm = new FormControl<number>(0, {nonNullable: true});
  isPrime = false;
  loading = false;

  constructor(private prime: PrimeAsyncService,
              private changeDetectorRef: ChangeDetectorRef) {
    this.numberForm.valueChanges
      .pipe(takeUntil(this.destroyed))
      .pipe(mergeMap(number => this.prime.isPrime(number)))
      .subscribe(isPrime => {
        this.isPrime = isPrime;
        this.changeDetectorRef.detectChanges();
      });
  }

  computeNextPrimeNumber(): void {
    this.loading = true;
    const number = this.numberForm.value;
    this.prime.nextPrime(number)
      .subscribe(next => {
        this.numberForm.setValue(next);
        this.loading = false;
        this.changeDetectorRef.detectChanges();
      });
  }

  ngOnDestroy(): void {
    this.destroyed.next(true);
    this.destroyed.complete();
  }

}

This component listens for changes made to the numberInput with this.numberForm.valueChanges and calls the async PrimeAsyncService with the pipe mergeMap(number => this.prime.isPrime(number)). The ChangeDetectorRef.detectChanges() method must be called when the state of the component is updated asynchronously as we use the OnPush change detection strategy.

The method computeNextPrimeNumber is called when the user clicks on the Next button. Here again it uses the PrimeAsyncService to get the next prime number value asynchronously using the underlying web-worker.

Web-Worker performance test

Let's check what are the benefits in terms of performances!

The Angular Devtools Profiler is the proper tool to know how long it takes to Angular for reacting to a click on the Next button and computing the next prime number value.

It shows us that it takes only 1ms to handle the click itself: Angular Profiler Click

And 0.2ms to update the component state once the web-worker has done computing the next prime value: Angular Profiler Worker Message

So we can conclude that using a web-worker has successfully prevented our Angular application from freezing even while doing an operation that can take several seconds of computing!

Want to become a super load tester?
Request a Demo