increasing productivity using WebAssembly

This post demonstrates how to easily use WebAssembly inside an application written in Angular. Sometimes in an Angular application you need to complete a task that is not completed very quickly in JavaScript. Of course, you can rewrite the algorithm in another language, for example AssemblyScript and Rust, and the code will become more efficient. You can then compile the resulting code snippets into a WASM file and stream the binary data into the application so that WASM functions can be called from it. It also happens that a developer is unable to find open source libraries needed to solve a problem in the NPM registry. In this case, you can write a package in a language other than JS, then compile this package into WASM and publish the WASM code to the NPM registry. Angular developers install the new package as a dependency and execute WASM functions inside the application.

In the following demo, I’ll write some functions in AssemblyScript to work with prime numbers, and then publish an index file in WASM format. I’ll then copy the WASM file into the Angular app, stream the binary data through the WebAssembly API, and finally call these functions to perform various actions on prime numbers.

What is WebAssembly?

WebAssembly is a compound word that is divided into 2 parts: Web (Web) and Assembly (Assembler). When you write in high-level languages, such as AssemblyScript and Rust, you end up with code that is compiled into assembler using special tools. After this, the developer can natively execute assembly code directly in the browser, that is, on the web.

How this demo can be used in practice

There are 2 github repositories associated with this demo. The first uses the AssemblyScript language, in which TypeScript-like code is written and compiled into Wasm. The second repository contains a simple Angular application that uses Wasm functions to explore some interesting properties of prime numbers.

The AssemblyScript repository index contains 3 functions for working with prime numbers:

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

I copied release.wasm into the Angular app’s assets directory, wrote a WebAssembly loader to stream the binary, and returned a WebAssembly instance. The master component links the instance to components where it acts as an input. These components use the instance to execute Wasm and utility functions to obtain the results of operations on prime numbers.

Writing WebAssembly in AssemblyScript

AssemblyScript is a TypeScript-like language in which you can write code that is later compiled into WebAssembly.

We are starting a new project

npm init

Installing the dependency

npm install --save-dev assemblyscript

Execute the command to add scripts to package.json and scaffold files

npx asinit .

Our own scripts for generating 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 ."
}

Implementing an algorithm for working with prime numbers in AssemblyScript

// assembly/index.ts

// Запись в файле, в этой записи указан модуль WebAssembly

// импорт модуля
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 to findFirstNPrimes. This function has no body, and it is the Angular application’s responsibility to provide the implementation details for it.

After running the npm run asbuild script, the builds/ directory will contain the debug.wasm and release.wasm files. The part of the work that concerns WebAssembly is already ready, and from now on we will only work with the Angular application.

Combining the strengths of WebAssembly and Angular

High-level data types such as arrays and booleans are not supported by WebAssembly. So I installed the assemblyscript loader and, using the helper functions it contains, cast the values ​​returned by the Wasm functions to the correct types.

Installing dependency

npm i @assemblyscript/loader

Building the WebAssembly loader

Next, through trial and error, I was able to import Wasm functions into the Angular application by streaming release.wasm using the assemblyscript loader.

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

I encapsulated the WebAssembly loader in a loading service, thereby allowing all Angular components to reuse the data streaming functionality. If the browser supports instantiateStreaming, an instance of WebAssembly is returned. If instantiateStreaming is not supported, a fallback function will be called. The fallback function converts the response into an ArrayBuffer and assembles the WebAssembly instance.

DEFAULT_IMPORTS also provides an implementation of primeNumberLog. primeNumberLog is declared in the index.ts file of the AssemblyScript repository. Therefore, the key of an object is its position in the index without 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;
  }
}

Linking a WebAssembly instance to Angular components

In AppComponent I streamed release.wasm to build the WebAssembly instance. I then associated this instance with the output from 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);
  }
}

Applying WebAssembly to Angular Components

IsPrimeComponent calls the isPrime function to determine whether the given integer is prime. isPrime returns 1 if it is a prime number, otherwise it returns 0. Therefore, the === operator compares integer values ​​to return a boolean as a result.

// 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 calls the findFirstNPrimes function to get the first N primes. The findFirstNPrimes function can’t transfer an integer array, so I use the __getArray helper function in the loader to convert the integer value into a valid 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 calls the optimizedSieve function to get all prime numbers that are less than to obtain N. The optimizedSieve function also cannot carry an integer array, so I use the __getArray helper function to convert the integer value into a valid 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 [];
  });
}

A completed example is shown on the next page:

railsstudent.github.io

That’s all. I hope you enjoyed this post and it inspires you to learn Angular and other technologies.

Resources:

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *