Powering Angular with Rust (Wasm)
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”:
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.