Getting Started with Rust and WebAssembly
Sam Rivera
January 22, 2025 · 6 min read
Rust and WebAssembly are a natural fit. Rust produces tiny, fast binaries with no garbage collector to pause execution, and the wasm-pack toolchain makes publishing Rust code as a Wasm npm package almost trivial.
By the end of this tutorial you will have:
- A working Rust library compiled to WebAssembly
- TypeScript-typed JavaScript bindings generated automatically
- A simple web page that calls your Wasm functions
- An optimised binary ready for production
Prerequisites
You'll need the following installed:
Install wasm-pack with Cargo:
cargo install wasm-packAdd the WebAssembly compilation target to your Rust toolchain:
rustup target add wasm32-unknown-unknownCreating the Project
Use cargo to create a new library crate:
cargo new --lib wasm-greeter
cd wasm-greeterOpen Cargo.toml and configure it for WebAssembly:
[package]
name = "wasm-greeter"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
[profile.release]
opt-level = "z" # optimise aggressively for binary size
lto = true # enable link-time optimisation
cdylibtells the Rust compiler to produce a dynamic library suitable for linking into a Wasm module.rlibkeeps the crate usable from other Rust code.
Understanding wasm-bindgen
wasm-bindgen is the glue between Rust and JavaScript. It generates JavaScript and TypeScript wrapper code so you can call Rust functions as if they were ordinary JS functions — passing strings, arrays, and structs without manually serialising bytes.
Writing the Rust Code
Replace src/lib.rs with the following:
use wasm_bindgen::prelude::*;
/// Returns a greeting string. Exported to JavaScript.
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello from Rust, {}! 🦀", name)
}
/// Computes the nth Fibonacci number iteratively.
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => {
let (mut a, mut b) = (0u32, 1u32);
for _ in 2..=n {
let c = a + b;
a = b;
b = c;
}
b
}
}
}
/// A simple counter struct exported to JavaScript.
#[wasm_bindgen]
pub struct Counter {
value: i32,
}
#[wasm_bindgen]
impl Counter {
#[wasm_bindgen(constructor)]
pub fn new(initial: i32) -> Counter {
Counter { value: initial }
}
pub fn increment(&mut self) { self.value += 1; }
pub fn decrement(&mut self) { self.value -= 1; }
pub fn get(&self) -> i32 { self.value }
}The #[wasm_bindgen] macro marks items for export. For each exported item it generates:
- A JavaScript wrapper function or class
- A TypeScript
.d.tsdeclaration - Any glue required to marshal types across the Wasm boundary
Building the Wasm Module
Run wasm-pack targeting the browser:
wasm-pack build --target webThis creates a pkg/ directory:
pkg/
├── wasm_greeter.js # JS glue code
├── wasm_greeter.d.ts # TypeScript declarations
├── wasm_greeter_bg.wasm # compiled Wasm binary
├── wasm_greeter_bg.wasm.d.ts
└── package.json
Build Targets
| Target | Use case |
|---|---|
web | Direct ES module import in browsers |
bundler | Webpack, Vite, Rollup — best for npm packages |
nodejs | Node.js (CommonJS) |
no-modules | Legacy browsers without ES module support |
Calling Wasm from JavaScript
Create a minimal web project alongside the Rust crate:
mkdir web && cd webweb/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Wasm Greeter</title>
</head>
<body>
<div id="output"></div>
<script type="module" src="./main.js"></script>
</body>
</html>web/main.js
import init, { greet, fibonacci, Counter } from '../pkg/wasm_greeter.js'
async function run() {
// Download and compile the .wasm binary, then instantiate the module
await init()
// Call an exported function
const message = greet('WasmHub')
document.getElementById('output').textContent = message
console.log(message) // → "Hello from Rust, WasmHub! 🦀"
// Compute Fibonacci numbers
for (let i = 0; i <= 10; i++) {
console.log(`fib(${i}) = ${fibonacci(i)}`)
}
// Use the exported Counter class
const counter = new Counter(0)
counter.increment()
counter.increment()
counter.increment()
counter.decrement()
console.log(`Counter: ${counter.get()}`) // → 2
// Free Wasm-allocated memory when you're done with a struct
counter.free()
}
run().catch(console.error)Serve with any static file server:
npx serve .Open http://localhost:3000 and check the browser console — you should see the greeting and Fibonacci sequence.
Optimising Binary Size
Out of the box the .wasm binary may be larger than necessary. Use wasm-opt (part of the Binaryen toolkit) to shrink it further:
# Install Binaryen
brew install binaryen # macOS
apt install binaryen # Debian / Ubuntu
# Aggressive size optimisation (-Oz)
wasm-opt -Oz \
pkg/wasm_greeter_bg.wasm \
-o pkg/wasm_greeter_bg.wasmCommon wasm-opt flags:
-O1/-O2/-O3— speed-oriented optimisation levels-Oz— aggressive size optimisation (smallest output)-Os— balanced size and speed
wasm-pack can run wasm-opt automatically; just pass --release:
wasm-pack build --target web --releaseDebugging Tips
Better Panic Messages
By default, Rust panics in Wasm silently abort. Add console_error_panic_hook to see stack traces in the browser console:
# Cargo.toml
[dependencies]
wasm-bindgen = "0.2"
console_error_panic_hook = "0.1"// src/lib.rs — call this once at startup
#[wasm_bindgen(start)]
pub fn main() {
console_error_panic_hook::set_once();
}Testing with wasm-bindgen-test
wasm-bindgen ships a test framework that runs your Rust tests inside a real browser:
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn fibonacci_is_correct() {
assert_eq!(fibonacci(0), 0);
assert_eq!(fibonacci(1), 1);
assert_eq!(fibonacci(10), 55);
}
}Run the tests headlessly:
wasm-pack test --headless --firefoxWhat's Next?
You now have a working Rust + Wasm project. From here, explore:
web-sys— Rust bindings for every Web API (DOM, Fetch, Canvas, WebGL…)gloo— Ergonomic, idiomatic wrappers aroundweb-sys- Trunk — A Wasm-first build tool for Rust web apps (Vite for Rust)
- Yew — A React-inspired component framework for Rust
Conclusion
Rust and WebAssembly unlock performance and reliability that JavaScript alone can't match for computationally heavy work. The wasm-pack toolchain removes most of the friction, giving you TypeScript bindings, npm integration, and optimised builds in a single command.
Browse the Directory for more Rust + Wasm tools, or experiment without any local setup in the Playground.