Angular on Steroids: Elevating Performance with WebAssembly

Connie Leung - Dec 25 '23 - - Dev Community

Introduction

In this blog post, I demonstrated how to use WebAssembly within an Angular application easily. In some cases, an Angular application wants to perform a task that is not fast in JavaScript. Developers can rewrite the algorithm in other languages such as AssemblyScript and Rust to write efficient codes. Then, the developers can compile the codes to WASM file, and stream the binary in the application to call the WASM functions. It is also possible that developers cannot find open source libraries in the NPM registry for the job. They have the option to write new package in non-JS languages, compile it into WASM and publish the WASM codes to NPM registry. Angular developers install the new package as dependency and execute the WASM functions within an application.

In the following demo, I wrote some prime number functions in AssemblyScript and published the index file into a WASM file. Then, I copied the WASM file to Angular application, streamed the binary with WebAssembly API and finally called these functions to perform various actions related to prime numbers.

What is WebAssembly?

WebAssembly can break down into 2 words: Web and Assembly. High level programming languages such as AssemblyScript and Rust write codes that are compiled into assembly by tools. Then, developers run the assembly codes natively on browser in the web.

Use case of the demo

This demo has 2 github repositories: The first repository uses AssemblyScript to write TypeScript-like codes that compile into Wasm. The second repository is a simple Angular application that uses the Wasm functions to explore some interesting facts of prime numbers

In the AssemblyScript repository, the index file has 3 prime number functions:

  • isPrime - Determine whether or not an integer is a prime number
  • findFirstNPrimes - Find the first N prime numbers where N is an integer
  • optimizedSieve - Find all the prime numbers less than N where N is an integer

AssemblyScript adds scripts in package.json that generate debug.wasm and release.wasm respectively.

I copied release.wasm to assets folder of the Angular application, wrote a WebAssembly loader to stream the binary file and return a WebAssembly instance. The main component bound the instance to the components as input, and these components used the instance to execute Wasm and utility functions to obtain prime number results.

Write WebAssembly in AssemblyScript

AssemblyScript is a TypeScript-like language that can write codes to compile into WebAssembly.

Start a new project

npm init
Enter fullscreen mode Exit fullscreen mode

Install dependency

npm install --save-dev assemblyscript
Enter fullscreen mode Exit fullscreen mode

Run command to add scripts in package.json and scaffold files

npx asinit .
Enter fullscreen mode Exit fullscreen mode

Custom scripts to generate debug.wasm and release.wasm files

"scripts": {
    "asbuild:debug": "asc assembly/index.ts --target debug --exportRuntime",
    "asbuild:release": "asc assembly/index.ts --target release --exportRuntime",
    "asbuild": "npm run asbuild:debug && npm run asbuild:release",
    "start": "npx serve ."
}
Enter fullscreen mode Exit fullscreen mode

Implement prime number algorithm in AssemblyScript

// assembly/index.ts

// The entry file of your WebAssembly module.

// module import
declare function primeNumberLog(primeNumber: i32): void;

export function isPrime(n: i32): bool {
  if (n <= 1) {
    return false;
  } else if (n === 2 || n === 3) {
    return true;
  } else if (n % 2 === 0 || n % 3 === 0) {
    return false;
  }

  for (let i = 5; i <= Math.sqrt(n); i = i + 6) {
    if (n % i === 0 || n % (i + 2) === 0) {
      return false;
    }
  }

  return true;
} 

export function findFirstNPrimes(n: i32): Array<i32> {
  let primes = new Array<i32>(n);

  for (let i = 0; i < n; i++) {
    primes[i] = 0;
  }
  primes[0] = 2;
  primeNumberLog(primes[0]);

  let num = 3;
  let index = 0;
  while(index < n - 1) {
    let isPrime = true;

    for (let i = 0; i <= index; i++) {
      if (num % primes[i] === 0) {
        isPrime = false;
        break;
      }
    }

    if (isPrime) {
      primeNumberLog(num);
      primes[index + 1] = num;
      index = index + 1;
    }
    num = num + 2;
  }

  return primes;
}

const MAX_SIZE = 1000001;

export function optimizedSieve(n: i32): Array<i32> {
  const isPrime = new Array<bool>(MAX_SIZE);
  isPrime.fill(true, 0, MAX_SIZE);

  const primes = new Array<i32>();
  const smallestPrimeFactors = new Array<i32>(MAX_SIZE);
  smallestPrimeFactors.fill(1, 0, MAX_SIZE);

  isPrime[0] = false;
  isPrime[1] = false;

  for (let i = 2; i < n; i++) {
    if (isPrime[i]) {
      primes.push(i);

      smallestPrimeFactors[i] = i;
    }

    for (let j = 0; j < primes.length && i * primes[j] < n && primes[j] <= smallestPrimeFactors[i]; j++) {
      const nonPrime = i * primes[j];
      isPrime[nonPrime] = false;
      smallestPrimeFactors[nonPrime] = primes[j];
    }
  }

  const results = new Array<i32>();
  for (let i = 0; i < primes.length && primes[i] <= n; i++) {
    results.push(primes[i]);
  }

  return results;
}
Enter fullscreen mode Exit fullscreen mode

primeNumberLog is an external function that logs prime numbers in findFirstNPrimes. The function has no body and Angular application is responsible for providing the implementation details.

After executing npm run asbuild script, the builds/ folder contains debug.wasm and release.wasm. The part with WebAssembly is done and I proceeded with Angular application.

Combine the power of WebAssembly and Angular

WebAssembly does not transfer high-level data types such as array and boolean. Therefore, I installed assemblyscript loader and applied its utility functions to convert the returned values of Wasm functions to the correct type.

Install dependency

npm i @assemblyscript/loader
Enter fullscreen mode Exit fullscreen mode

Build a WebAssembly loader

After trials and errors, the Angular application was able to import the Wasm functions by streaming release.wasm with a assemblyscript loader.

src
├── assets
 │   └── release.wasm
├── favicon.ico
├── index.html
├── main.ts
└── styles.scss
Enter fullscreen mode Exit fullscreen mode

I encapsulated the loader in a WebAssembly loader service such that all Angular components can reuse the streaming functionality. If browser supports instantiateStreaming function, a WebAssembly instance is returned. If instantiateStreaming function is unsupported, the fallback will be called. The fallback converts the response to an array buffer and constructs a WebAssembly instance.

DEFAULT_IMPORTS also supplies the implementation of primeNumberLog. primeNumberLog is declared in index.ts of the AssemblyScript repository; therefore, the object key is index without the file extension.

// web-assembly-loader.service.ts

import { Injectable } from '@angular/core';
import loader, { Imports } from '@assemblyscript/loader';

const DEFAULT_IMPORTS: Imports = { 
  env: {
    abort: function() {
      throw new Error('Abort called from wasm file');
    },
  },
  index: {
    primeNumberLog: function(primeNumber: number) {
      console.log(`primeNumberLog: ${primeNumber}`);
    }
  }
}

@Injectable({
  providedIn: 'root'
})
export class WebAssemblyLoaderService {
  async streamWasm(wasm: string, imports = DEFAULT_IMPORTS): Promise<any> {
    if (!loader.instantiateStreaming) {
      return this.wasmFallback(wasm, imports);
    }

    const instance = await loader.instantiateStreaming(fetch(wasm), imports);
    return instance?.exports;
  }

  async wasmFallback(wasm: string, imports: Imports) {
    console.log('using fallback');
    const response = await fetch(wasm);
    const bytes = await response?.arrayBuffer();
    const { instance } = await loader.instantiate(bytes, imports);

    return instance?.exports;
  }
}
Enter fullscreen mode Exit fullscreen mode

Bind WebAssembly instance to Angular Components

In AppComponent, I streamed release.wasm to construct a WebAssembly instance. Then, I bound the instance to the input of the Angular Components.

// app.config.ts

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_BASE_HREF,
      useFactory: () => inject(PlatformLocation).getBaseHrefFromDOM(),
    }
  ]
};
Enter fullscreen mode Exit fullscreen mode
// full-asset-path.ts

export const getFullAssetPath = (assetName: string) => {
    const baseHref = inject(APP_BASE_HREF);
    const isEndWithSlash = baseHref.endsWith('/');
    return `${baseHref}${isEndWithSlash ? '' : '/'}assets/${assetName}`;
}
Enter fullscreen mode Exit fullscreen mode
// app.component.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [FormsModule, IsPrimeComponent, FindFirstNPrimesComponent, OptimizedSieveComponent],
  template: `
    <div class="container outer" style="margin: 0.5rem;">
      <h2>Angular + WebAssembly Demo</h2>
      <app-is-prime [instance]="instance" />
      <app-find-first-nprimes [instance]="instance" />
      <app-optimized-sieve [instance]="instance" />
    </div>
  `,
})
export class AppComponent implements OnInit {
  instance!: any;
  releaseWasm = getFullAssetPath('release.wasm');
  wasmLoader = inject(WebAssemblyLoaderService);

  async ngOnInit(): Promise<void> {
    this.instance = await this.wasmLoader.streamWasm(this.releaseWasm);
    console.log(this.instance);
  }
}
Enter fullscreen mode Exit fullscreen mode

Apply WebAssembly to Angular Components

IsPrimeComponent invokes isPrime function to determine whether or not an integer is a prime number. isPrime returns 1 when it is a prime number and 0, otherwise. Therefore, === operator compares the integer values to return a boolean.

// is-prime.component.ts

@Component({
  selector: 'app-is-prime',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form>
      <label for="primeNumber">
        <span>Input an positive integer: </span>
        <input id="primeNumber" name="primeNumber" type="number"
          [ngModel]="primeNumber()" (ngModelChange)="primeNumber.set($event)" />
      </label>
    </form>
    <p class="bottom-margin">isPrime({{ primeNumber() }}): {{ isPrimeNumber() }}</p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IsPrimeComponent {
  @Input({ required: true })
  instance!: any;

  primeNumber = signal(0);

  isPrimeNumber = computed(() => { 
    const value = this.primeNumber();
    return this.instance ? this.instance.isPrime(value) === 1 : false
  });
}
Enter fullscreen mode Exit fullscreen mode

FindFirstNPrimesComponent invokes findFirstNPrimes function to obtain the first N prime numbers. findFirstNPrimes cannot transfer integer array; therefore, I apply __getArray utility function of the loader to convert the integer value to correct integer array.

// find-first-nprimes.component.ts

@Component({
  selector: 'app-find-first-nprimes',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form>
      <label for="firstNPrimeNumbers">
        <span>Find first N prime numbers: </span>
        <input id="firstNPrimeNumbers" name="firstNPrimeNumbers" type="number"
          [ngModel]="firstN()" (ngModelChange)="firstN.set($event)" />
      </label>
    </form>

    <p class="bottom-margin">First {{ firstN() }} prime numbers:</p>
    <div class="container first-n-prime-numbers bottom-margin">
      @for(primeNumber of firstNPrimeNumbers(); track primeNumber) {
        <span style="padding: 0.25rem;">{{ primeNumber }}</span>
      }
    <div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FindFirstNPrimesComponent {
  @Input({ required: true })
  instance!: any;

  firstN = signal(0);

  firstNPrimeNumbers = computed(() => {
    const value = this.firstN();
    if (this.instance) {
      const { findFirstNPrimes, __getArray: getArray } = this.instance;
      return getArray(findFirstNPrimes(value));
    }

    return [];
  });
}
Enter fullscreen mode Exit fullscreen mode

OptimizedSieveComponent invokes optimizedSieve function to obtain all prime numbers that are less than N. Similarly, optimizedSieve cannot transfer integer array and I apply __getArray utility function to convert the integer value to correct integer array.

// optimized-sieve.component.ts

@Component({
  selector: 'app-optimized-sieve',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form>
      <label for="primeNumber">
        <span>Input an positive integer: </span>
        <input id="primeNumber" name="primeNumber" type="number"
          [ngModel]="lessThanNumber()" (ngModelChange)="lessThanNumber.set($event)" />
      </label>
    </form>

    <p class="bottom-margin">Prime numbers less than {{ lessThanNumber() }}</p>
    <div class="container prime-numbers-less-than-n bottom-margin">
      @for(primeNumber of primeNumbers(); track primeNumber) {
        <span style="padding: 0.25rem;">{{ primeNumber }}</span>
      }
    <div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OptimizedSieveComponent {
  @Input({ required: true })
  instance!: any;

  lessThanNumber = signal(0);

  primeNumbers = computed(() => { 
    const value = this.lessThanNumber();
    if (this.instance) {
      const { optimizedSieve, __getArray: getArray } = this.instance;
      return getArray(optimizedSieve(value));
    }

    return [];
  });
}
Enter fullscreen mode Exit fullscreen mode

The following Github page shows the final results:

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

Resources:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .