Powering Angular with Rust (Wasm)

🪄 OZ 🎩
6 min readJun 11, 2024

--

“Red Sunset on the Dnipro”, Arkhip Kuindzhi, 1905

This article explains how to set up and start using Rust in your Angular application.

Why?

If the code of your app has parts where you need to work with a large amount of data (especially numbers), WebAssembly can do it much faster.

Examples include drawing, computing dynamic reports, complicated calculations (geodesy, astronomy, physics…), cryptography, LLM, image editing, video processing, games, and so on.

In this article, we’ll create an Angular workspace with an application and a library, written in Rust. Rust library we will compile into the WebAssembly (Wasm) module, and our Angular app will use this module.

Installation

  • Install Rust. Visit https://www.rust-lang.org/tools/install and you’ll find a command to install rustup. I know, an option that says “run some script fetched by curl” might look suspicious, but it is a safe and very convenient way. Currently, the command is:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  • Install wasm-pack:
cargo install wasm-pack

Angular Workspace

There are multiple ways to create an Angular workspace, and I would recommend Nx, but let’s not increase the complexity of this article and use the required tools only.

Don’t forget to update your Angular CLI:

npm i -g @angular/cli

In your terminal, navigate to the folder where your workspace should be created, and run:

ng new ng-wasm-example --no-create-application

Go to that new folder and generate an app:

cd ng-wasm-example
ng g application example-app

There are multiple ways how to use a Wasm module in our Angular app, and we’ll go with the easiest* one: inside our Angular workspace, we’ll create a regular Angular library, that will be a wrapper for our Rust library. This wrapper will also export types and a function to initialize our Wasm module.

ng g library wasm-example

*We could create a Rust library directly inside our app, and it would be even easier to compile .wasm file to some assets folder. But then other libraries of our application could not use this Wasm module, so it’s not a scalable and maintainable approach.

Rust Library

Let’s create a Rust library inside our Angular :

cd projects/wasm-example/src/lib
cargo new --lib example-rust-lib --vcs none

Option --vcs none will prevent the creation of a git repository — we are already inside an Angular workspace repository.

Now let’s edit Cargo.toml file of our library:

[package]
name = "example-rust-lib"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[profile.release]
lto = true
#opt-level = 's'

[dependencies]
wasm-bindgen = "0.2"

We’ve added [lib] section to specify crate-type, and added wasm-bindgen dependency.

In lib.rs replace content with this line:

use wasm_bindgen::prelude::*;

This line runs a bunch of tools to create a communication bridge between JavaScript and Rust.

Now you are ready to write your next masterpiece in Rust.

But that’s an exercise for you, dear reader. This article will go the classic way and calculate something useless, but computationally expensive:

use wasm_bindgen::prelude::*;

pub fn factorial(num: u128) -> u128 {
match num {
0 => 1,
1 => 1,
_ => factorial(num - 1) * num,
}
}


#[wasm_bindgen]
pub fn get_factorial(num: u8) -> String {
let mut f: u128 = 0;
for _ in 0..10000000 {
f = factorial(num as u128);
}
f.to_string()
}

The get_factorial function has an instruction above that tells wasm_bindgen to make this function callable from JavaScript.

Now save your files and create a package:

cd example-rust-lib
wasm-pack build --target web

It will take some time when you run it for the first time.

The second command (wasm-pack build — target web) we have to run every time we modify our Rust library.

wasm-pack will generate pkg folder with a few files:

📂 pkg
├── 📄 .gitignore
├── 📄 example_rust_lib.d.ts
├── 📄 example_rust_lib.js
├── 📄 example_rust_lib_bg.wasm
├── 📄 example_rust_lib_bg.wasm.d.ts
└── 📄 package.json

File example_rust_lib_bg.wasm is our Wasm module!

Now in our wrapper library, let’s export information about get_factorial.

In file projects/wasm-example/src/public-api.ts (public API of our wrapper library), let’s replace content with:

import init from './lib/example-rust-lib/pkg';
export { get_factorial } from './lib/example-rust-lib/pkg';
export { init as initExampleRust} ;

Replace the name of the last export (initExampleRust) with something more suitable for your library, but don’t export it just as init, because you might have multiple libraries.

In the .gitignore file generated by wasm-pack (projects/wasm-example/src/lib/example-rust-lib/pkg/.gitignore), replace * with *.wasm, because other files are quite useful for us (they provide types).

That’s it, our library is ready to use!

Preparing our workspace

In tsconfig.json (that is located in our repository root), modify “paths”:

    "paths": {
"wasm-example": [
"./projects/wasm-example/src/public-api.ts"
]
}

This way we can get code completion in IDE without re-building a library.

Our app will need the .wasm file that our library generates, let’s declare it in angular.json.
Add path to .wasm files, generated by example-rust-lib, into assets option:

{
"projects": {
"example-app": {
"architect": {
"build": {
"options": {
"assets": [
{
"glob": "**/*.wasm",
"input": "projects/wasm-example/src/lib/example-rust-lib/pkg"
}
...

Calling Rust function in Angular app

After all these preparations, we can finally start using our awesome Rust library, our masterpiece!

Replace the content of projects/example-app/src/app/app.component.html with this:

<input max="22" min="1" type="number" placeholder="Input a number" #inp/>

<button
type="button"
(click)="calculate(inp.value)"
[disabled]="calculating()"
>
Calculate
</button>

@if (!calculating()) {
@if (jsResult()) {
<div>Result in JS: {{ jsResult() }}, calculated in {{ jsTime() }}</div>
}
@if (rsResult()) {
<div>Result in Rust: {{ rsResult() }}, calculated in {{ rsTime() }}</div>
}
}

For app.component.scss:

:host {
display: flex;
flex-flow: column;
gap: 1em;

input {
width: 10em;
padding: 0.75em;
}

button {
padding: 0.5em;
width: 11.75em;
}
}

And in app.component.ts we are initializing (loading) our Wasm module and then using it:

import { ChangeDetectionStrategy, Component, type OnInit, signal } from '@angular/core';
import { get_factorial, initExampleRust } from 'wasm-example';

@Component({
selector: 'app-root',
standalone: true,
imports: [],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnInit {
jsResult = signal<string>('');
rsResult = signal<string>('');
jsTime = signal<string>('');
rsTime = signal<string>('');
calculating = signal<boolean>(false);

ngOnInit() {
initExampleRust();
}

calculate(inp: number | string) {
this.calculating.set(true);

setTimeout(() => {
const n = typeof inp === 'number' ? inp : parseInt(inp, 10);
const jsTimeStart = performance.now();
let f = 0;
for (let i = 0; i < 10000000; i++) {
f = factorial(n);
}
this.jsResult.set(f.toString());
this.jsTime.set(((performance.now() - jsTimeStart) / 1000).toFixed(4) + 's');

const rsTimeStart = performance.now();
this.rsResult.set(get_factorial(n));
this.rsTime.set(((performance.now() - rsTimeStart) / 1000).toFixed(4) + 's');

this.calculating.set(false);
}, 50);
}
}


function factorial(x: number): number {
if (x === 0) {
return 1;
} else {
return x * factorial(x - 1);
}
}

You can notice, that in our Rust code and our TypeScript code, we are calculating the same factorial 1 000 000 times, using the same algorithm without optimizations. That’s because otherwise, we would spend more time on the overhead of calling functions from a Wasm module.

In your terminal, run ng serve and in the opened app try to input “22”:

And now it works!

This article only explains how to start using Rust in your Angular apps. The real usage will require much more knowledge, and one article is not enough to explain all the details about wasm-bindgen, WebAssembly modules, their size optimizations, and other things.

But now you have a starting point — the first step is always the hardest.

References

--

--