Angular on Steroids: Elevating Performance with WebAssembly
Connie Leung
Posted on December 25, 2023
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
Install dependency
npm install --save-dev assemblyscript
Run command to add scripts in package.json and scaffold files
npx asinit .
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 ."
}
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;
}
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
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
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;
}
}
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(),
}
]
};
// full-asset-path.ts
export const getFullAssetPath = (assetName: string) => {
const baseHref = inject(APP_BASE_HREF);
const isEndWithSlash = baseHref.endsWith('/');
return `${baseHref}${isEndWithSlash ? '' : '/'}assets/${assetName}`;
}
// 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);
}
}
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
});
}
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 [];
});
}
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 [];
});
}
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:
- Github Repo of WebAssembly: https://github.com/railsstudent/prime-number-wasm
- Github Repo of Angular + WebAssembly demo: https://github.com/railsstudent/ng-webassembly-demo
- Live demo: https://railsstudent.github.io/ng-webassembly-demo/
- AssemblyScript: https://angular.dev/guide/templates/control-flow#if-block-conditionals
- WebAssembly API: https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/instantiateStreaming_static
Posted on December 25, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.