Rust & WebAssembly
WebAssembly (Wasm) is a compact binary instruction format that every modern browser can run at close to native speed, inside the same safe sandbox as JavaScript. It is not a replacement for JavaScript: JavaScript still drives the page, and Wasm takes over jobs that need raw speed or code you already have in another language, such as image editing, PDF generation, games, maths and data validation.
Rust is one of the best languages for Wasm because it needs no garbage collector and no large runtime, so the files stay small. In this lesson you will build a loan EMI calculator in Rust, call it from a web page, run another Rust program outside the browser with WASI, and learn how to shrink the output.
Wasm targets in Rust
Rust produces Wasm through ordinary compilation targets, just like it produces programs for Windows or Linux. The ones you will meet:
| Target | Used for | Standard library |
|---|---|---|
wasm32-unknown-unknown | Browsers and JavaScript hosts, usually with wasm-bindgen | Yes, but no files, clock or threads by itself |
wasm32-wasip1 | Command-line programs on WASI runtimes (preview 1 API) | Yes |
wasm32-wasip2 | WASI 0.2 and the Component Model | Yes |
wasm32v1-none | Minimal Wasm without std | No (no_std only) |
All four are Tier 2 targets, which means the Rust project builds them and ships them through rustup. Install a target once:
rustup target add wasm32-unknown-unknown
rustup target add wasm32-wasip2WASI (WebAssembly System Interface) is a standard set of functions that lets Wasm programs read files, get the time and print text outside a browser, with permissions granted by the runtime.
Building an EMI calculator for the browser
An EMI (equated monthly instalment) is the fixed amount a borrower pays every month. For a loan of P rupees at a monthly interest rate r (annual rate divided by 12 and by 100) over n months, EMI = P x r x (1 + r)^n / ((1 + r)^n - 1).
Step 1: create a library crate
cargo new --lib emi-calc
cd emi-calc
cargo add wasm-bindgenThen tell Cargo what kind of library to produce, in Cargo.toml:
[lib]
crate-type = ["cdylib", "rlib"]cdylib produces a standalone .wasm file that other languages can load. rlib is the normal Rust library format; keeping it means cargo test and other Rust crates can still use the code.
Step 2: write and export the functions
Replace src/lib.rs:
use wasm_bindgen::prelude::*;
/// Monthly instalment for a loan.
#[wasm_bindgen]
pub fn emi(principal: f64, annual_rate: f64, months: u32) -> f64 {
if months == 0 {
return 0.0;
}
let r = annual_rate / 12.0 / 100.0;
if r == 0.0 {
return principal / f64::from(months);
}
let growth = (1.0 + r).powi(months as i32);
principal * r * growth / (growth - 1.0)
}
/// A ready-to-show sentence for the web page.
#[wasm_bindgen]
pub fn emi_summary(principal: f64, annual_rate: f64, months: u32) -> String {
let monthly = emi(principal, annual_rate, months);
let interest = monthly * f64::from(months) - principal;
format!("EMI Rs. {monthly:.2}, total interest Rs. {interest:.2}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn five_lakh_at_nine_percent_for_five_years() {
let monthly = emi(500_000.0, 9.0, 60);
assert!((monthly - 10_379.18).abs() < 0.01);
}
}#[wasm_bindgen] marks a function as visible to JavaScript and generates the glue code that converts values. Numbers pass straight through (a JavaScript number is an f64), and String results are copied into a JavaScript string for you. The functions are still plain Rust, so cargo test runs the test on your own computer without any browser.
Step 3: compile to Wasm
cargo build --release --target wasm32-unknown-unknownThis writes target/wasm32-unknown-unknown/release/emi_calc.wasm. Notice the underscore: library file names use the crate name, where hyphens become underscores.
Step 4: generate the JavaScript glue
The raw .wasm file only understands numbers. The wasm-bindgen CLI reads it and writes the JavaScript that handles strings and other types:
cargo install wasm-bindgen-cli --version 0.2.130
wasm-bindgen --target web --out-dir pkg target/wasm32-unknown-unknown/release/emi_calc.wasmThe CLI version must match the wasm-bindgen version in your Cargo.lock exactly, otherwise it refuses to work; check Cargo.lock and install that version. --target web produces an ES module that a browser can load without a bundler. The pkg folder now holds emi_calc.js, emi_calc_bg.wasm and TypeScript .d.ts type files.
Step 5: call it from a page
Create index.html next to pkg:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>EMI calculator</title>
</head>
<body>
<script type="module">
import init, { emi, emi_summary } from "./pkg/emi_calc.js";
await init();
console.log(emi(500000, 9, 60).toFixed(2));
console.log(emi_summary(500000, 9, 60));
</script>
</body>
</html>10379.18 EMI Rs. 10379.18, total interest Rs. 122750.66
The default export init downloads and starts the Wasm module; you must await it before calling any exported function. Browsers do not load modules from file:// addresses, so serve the folder with any local web server, for example python -m http.server 8000, and open http://localhost:8000 with the developer console visible. A loan of Rs. 5,00,000 at 9% for 60 months costs Rs. 10,379.18 a month.
Wasm outside the browser with WASI
The same toolchain builds normal command-line programs for WASI runtimes such as Wasmtime. They run in a sandbox: by default they cannot touch your files or network unless the runtime grants it. That makes Wasm useful for plugins and for running untrusted code on servers.
// src/main.rs of a binary crate called hello-wasi
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let name = args.first().map(String::as_str).unwrap_or("friend");
println!("Namaste, {name}! This program is running as WebAssembly.");
}cargo build --release --target wasm32-wasip2
wasmtime target/wasm32-wasip2/release/hello-wasi.wasm KabirNamaste, Kabir! This program is running as WebAssembly.
Binary file names keep the package name exactly, hyphen included. The .wasm file is portable: the same file runs on Windows, Linux or macOS wherever Wasmtime is installed.
Keeping Wasm files small
Every kilobyte has to be downloaded before the page can use it, so size matters more than in a desktop program. Add a release profile tuned for size:
[profile.release]
opt-level = "z" # optimise for size ("s" is a milder option)
lto = true # optimise across all crates at link time
codegen-units = 1 # slower build, smaller and faster outputThen run wasm-opt -Oz from the Binaryen toolkit on the final .wasm for a further cut. Also watch your dependencies: formatting large numbers with format! and pulling in big crates both add code. Measure with the file size before and after each change.
For whole web applications written in Rust, frameworks such as Leptos, Yew and Dioxus build on wasm-bindgen. For most teams, though, the sweet spot is a small Rust module doing heavy work behind a normal JavaScript or TypeScript front end.
Common mistakes
- Forgetting
crate-type = ["cdylib"]. Without it,cargo buildproduces no.wasmfile you can load. - A wasm-bindgen CLI version that does not match the crate. Install the exact version listed in
Cargo.lock. - Calling exports before
await init(). The module is not ready yet, so the call fails. - Opening
index.htmlby double-clicking. Modules and Wasm need a web server; use a local one. - Expecting Wasm to make everything faster. Crossing between JavaScript and Wasm has a cost; send big chunks of work, not thousands of tiny calls.
Key points
- WebAssembly is a portable, sandboxed binary format; Rust compiles to it through targets installed with
rustup target add. wasm32-unknown-unknownwith#[wasm_bindgen]is the browser path;wasm32-wasip1andwasm32-wasip2are for WASI runtimes such as Wasmtime.- A browser library needs
crate-type = ["cdylib", "rlib"], a release build, and the wasm-bindgen CLI at the same version as the crate. - With
--target web, JavaScript imports the generated module and awaits its defaultinitfunction before calling exports. - Shrink output with
opt-level = "z",lto = trueandwasm-opt, and keep logic testable as plain Rust.
Exercise
Extend the EMI calculator for Isha's loan comparison page:
- Add an exported function
total_payable(principal: f64, annual_rate: f64, months: u32) -> f64that returns EMI times months. - Add a Rust unit test for a Rs. 50,000 loan at 12% for 12 months (EMI about Rs. 4,442.44).
- Write the two commands that rebuild the module and regenerate the
pkgfolder. - Show the JavaScript line that prints the total with two decimals.
Show solution
#[wasm_bindgen]
pub fn total_payable(principal: f64, annual_rate: f64, months: u32) -> f64 {
emi(principal, annual_rate, months) * f64::from(months)
}
#[test]
fn fifty_thousand_at_twelve_percent() {
let monthly = emi(50_000.0, 12.0, 12);
assert!((monthly - 4_442.44).abs() < 0.01);
let total = total_payable(50_000.0, 12.0, 12);
assert!((total - 53_309.27).abs() < 0.01);
}cargo build --release --target wasm32-unknown-unknown
wasm-bindgen --target web --out-dir pkg target/wasm32-unknown-unknown/release/emi_calc.wasmconsole.log(total_payable(50000, 12, 12).toFixed(2));53309.27
Put the test inside the existing tests module. total_payable reuses emi, so both functions always agree. The monthly rate is 1%, the EMI is Rs. 4,442.44 and twelve instalments add up to Rs. 53,309.27, so the interest is Rs. 3,309.27.
Quick Quiz
1. Which target do you use for Rust code that runs in a browser with wasm-bindgen?
The browser has no WASI system interface; wasm-bindgen generates JavaScript glue for the unknown-unknown target.
2. Why must a browser Wasm library set crate-type = ["cdylib", ...]?
A cdylib is a dynamic library for foreign callers, which for Wasm means a loadable module.
3. What must JavaScript do before calling an exported function from a --target web module?
init fetches and instantiates the .wasm file; exports are usable only after it finishes.
4. What replaced the wasm32-wasi target name?
The target was renamed in Rust 1.78 and the old name was removed in Rust 1.84.
Learn Rust with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
