Why Rust for WebAssembly?
Rust is the premier language for WebAssembly development, offering:
- Memory Safety: Compile-time guarantees without garbage collection
- Performance: Zero-cost abstractions and optimal code generation
- Tooling: Excellent wasm-pack and wasm-bindgen support
- Ecosystem: Crates for web development (wasm-bindgen, js-sys, web-sys)
Rust + WebAssembly Advantages:
✓ No runtime overhead
✓ Predictable performance
✓ Small binary sizes
✓ Type-safe JavaScript interop
✓ Modern async/await supportProject Setup
Installation
bash
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Add WebAssembly target
rustup target add wasm32-unknown-unknown
# Install wasm-pack (build tool)
cargo install wasm-pack
# Install cargo-edit (for dependency management)
cargo install cargo-editCreate New Project
bash
# Create new library project
cargo new --lib my-wasm-module
cd my-wasm-module
# Initialize for WebAssembly
wasm-pack init
# Or create with template
cargo generate --git https://github.com/rustwasm/wasm-pack-templateConfigure Cargo.toml
toml
[package]
name = "my-wasm-module"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <you@example.com>"]
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
"console",
"Window",
"Document",
"HtmlElement",
"Performance",
] }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"
[dev-dependencies]
wasm-bindgen-test = "0.3"
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1Basic Exports
Simple Functions
rust
use wasm_bindgen::prelude::*;
// Simple function
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// String manipulation
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// Boolean return
#[wasm_bindgen]
pub fn is_even(n: i32) -> bool {
n % 2 == 0
}
// Multiple return values (using tuple)
#[wasm_bindgen]
pub fn min_max(numbers: &[i32]) -> JsValue {
if numbers.is_empty() {
return JsValue::NULL;
}
let min = numbers.iter().min().unwrap();
let max = numbers.iter().max().unwrap();
// Convert to JavaScript object
let result = js_sys::Object::new();
js_sys::Reflect::set(&result, &"min".into(), &(*min).into()).unwrap();
js_sys::Reflect::set(&result, &"max".into(), &(*max).into()).unwrap();
result.into()
}Working with Arrays
rust
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn sum_array(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}
#[wasm_bindgen]
pub fn double_array(numbers: &[i32]) -> Vec<i32> {
numbers.iter().map(|x| x * 2).collect()
}
#[wasm_bindgen]
pub fn filter_even(numbers: &[i32]) -> Vec<i32> {
numbers.iter()
.filter(|x| x % 2 == 0)
.cloned()
.collect()
}
// Large array processing (avoid copying)
#[wasm_bindgen]
pub fn process_large_array(
input: &[f32],
output: &mut [f32],
multiplier: f32
) {
for (i, &val) in input.iter().enumerate() {
output[i] = val * multiplier;
}
}Structs and Classes
Define Structs
rust
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct Point {
x: f64,
y: f64,
}
#[wasm_bindgen]
impl Point {
#[wasm_bindgen(constructor)]
pub fn new(x: f64, y: f64) -> Point {
Point { x, y }
}
#[wasm_bindgen(getter)]
pub fn x(&self) -> f64 {
self.x
}
#[wasm_bindgen(setter)]
pub fn set_x(&mut self, x: f64) {
self.x = x;
}
#[wasm_bindgen(getter)]
pub fn y(&self) -> f64 {
self.y
}
#[wasm_bindgen(setter)]
pub fn set_y(&mut self, y: f64) {
self.y = y;
}
pub fn distance_from(&self, other: &Point) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
pub fn add(&self, other: &Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}State Management
rust
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct Counter {
count: i32,
step: i32,
}
#[wasm_bindgen]
impl Counter {
#[wasm_bindgen(constructor)]
pub fn new(initial: i32, step: i32) -> Counter {
Counter {
count: initial,
step,
}
}
pub fn increment(&mut self) {
self.count += self.step;
}
pub fn decrement(&mut self) {
self.count -= self.step;
}
pub fn reset(&mut self, value: i32) {
self.count = value;
}
pub fn get_count(&self) -> i32 {
self.count
}
pub fn set_step(&mut self, step: i32) {
self.step = step;
}
}Advanced Patterns
Error Handling
rust
use wasm_bindgen::prelude::*;
use std::fmt;
#[derive(Debug)]
pub enum WasmError {
InvalidInput(String),
CalculationFailed(String),
}
impl fmt::Display for WasmError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
WasmError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
WasmError::CalculationFailed(msg) => write!(f, "Calculation failed: {}", msg),
}
}
}
impl From<WasmError> for JsValue {
fn from(error: WasmError) -> JsValue {
JsValue::from_str(&error.to_string())
}
}
#[wasm_bindgen]
pub fn divide(a: f64, b: f64) -> Result<f64, JsValue> {
if b == 0.0 {
return Err(WasmError::InvalidInput("Cannot divide by zero".to_string()).into());
}
Ok(a / b)
}
#[wasm_bindgen]
pub fn parse_and_calculate(input: &str) -> Result<i32, JsValue> {
let num: i32 = input.parse()
.map_err(|_| WasmError::InvalidInput("Not a valid number".to_string()))?;
if num < 0 {
return Err(WasmError::InvalidInput("Number must be positive".to_string()).into());
}
Ok(num * 2)
}Async Operations
rust
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
use js_sys::Promise;
use web_sys::{Request, RequestInit, RequestMode, Response};
#[wasm_bindgen]
pub fn fetch_data(url: &str) -> Promise {
future_to_promise(async move {
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::Cors);
let request = Request::new_with_str_and_init(url, &opts)
.map_err(|e| JsValue::from_str(&format!("Request failed: {:?}", e)))?;
let window = web_sys::window().unwrap();
let response = window.fetch_with_request(&request)
.await
.map_err(|e| JsValue::from_str(&format!("Fetch failed: {:?}", e)))?;
let response: Response = response.dyn_into()
.map_err(|_| JsValue::from_str("Invalid response"))?;
let text = response.text()
.await
.map_err(|e| JsValue::from_str(&format!("Text conversion failed: {:?}", e)))?;
Ok(JsValue::from_str(&text))
})
}Working with DOM
rust
use wasm_bindgen::prelude::*;
use web_sys::{console, window, Document, Element, HtmlElement};
#[wasm_bindgen]
pub fn log_to_console(message: &str) {
console::log_1(&message.into());
}
#[wasm_bindgen]
pub fn update_element(element_id: &str, content: &str) -> Result<(), JsValue> {
let window = window().ok_or("No window")?;
let document = window.document().ok_or("No document")?;
let element = document.get_element_by_id(element_id)
.ok_or("Element not found")?;
element.set_inner_html(content);
Ok(())
}
#[wasm_bindgen]
pub fn create_div(class_name: &str, text: &str) -> Result<Element, JsValue> {
let window = window().ok_or("No window")?;
let document = window.document().ok_or("No document")?;
let div = document.create_element("div")?;
div.set_class_name(class_name);
div.set_inner_html(text);
Ok(div)
}
#[wasm_bindgen]
pub fn measure_performance() -> Result<f64, JsValue> {
let window = window().ok_or("No window")?;
let performance = window.performance().ok_or("No performance API")?;
Ok(performance.now())
}Performance Optimization
Memory Management
rust
use wasm_bindgen::prelude::*;
// Avoid allocations in hot path
#[wasm_bindgen]
pub struct ImageProcessor {
buffer: Vec<u8>,
width: usize,
height: usize,
}
#[wasm_bindgen]
impl ImageProcessor {
#[wasm_bindgen(constructor)]
pub fn new(width: usize, height: usize) -> ImageProcessor {
ImageProcessor {
buffer: vec![0; width * height * 4], // RGBA
width,
height,
}
}
// Direct buffer access for zero-copy operations
pub fn get_buffer_ptr(&self) -> *const u8 {
self.buffer.as_ptr()
}
pub fn get_buffer_length(&self) -> usize {
self.buffer.len()
}
// Process in place (avoid allocations)
pub fn invert_colors(&mut self) {
for pixel in self.buffer.chunks_exact_mut(4) {
pixel[0] = 255 - pixel[0]; // R
pixel[1] = 255 - pixel[1]; // G
pixel[2] = 255 - pixel[2]; // B
// Alpha unchanged
}
}
}SIMD Operations
rust
use wasm_bindgen::prelude::*;
use std::arch::wasm32::*;
#[wasm_bindgen]
pub fn vector_add_simd(a: &[f32], b: &[f32]) -> Vec<f32> {
assert_eq!(a.len(), b.len());
let mut result = vec![0.0f32; a.len()];
unsafe {
let chunks = a.len() / 4;
for i in 0..chunks {
let i = i * 4;
let a_vec = v128_load(a.as_ptr().add(i) as *const v128);
let b_vec = v128_load(b.as_ptr().add(i) as *const v128);
let sum = f32x4_add(a_vec, b_vec);
v128_store(result.as_mut_ptr().add(i) as *mut v128, sum);
}
// Handle remaining elements
for i in (chunks * 4)..a.len() {
result[i] = a[i] + b[i];
}
}
result
}Build and Deploy
Build Commands
bash
# Development build (no optimization)
wasm-pack build --dev --target web
# Production build (optimized)
wasm-pack build --release --target web
# Different targets
wasm-pack build --target bundler # webpack, rollup
wasm-pack build --target nodejs # Node.js
wasm-pack build --target web # Browser (native modules)
wasm-pack build --target no-modules # Script tag (no bundler)Optimize Binary Size
bash
# Use wasm-opt for additional optimization
wasm-opt pkg/my_wasm_bg.wasm -O4 -o pkg/my_wasm_bg_opt.wasm
# Enable Link-Time Optimization in Cargo.toml
[profile.release]
lto = true
opt-level = "z"
codegen-units = 1
# Remove debug symbols
[profile.release]
debug = false
strip = trueJavaScript Integration
javascript
// Modern ES modules
import init, { add, Point, Counter } from './pkg/my_wasm.js';
async function run() {
// Initialize WASM
await init();
// Use functions
console.log(add(5, 3)); // 8
// Use classes
const p1 = new Point(10, 20);
const p2 = new Point(30, 40);
console.log(p1.distance_from(p2)); // 28.284271247461902
// State management
const counter = new Counter(0, 5);
counter.increment();
console.log(counter.get_count()); // 5
}
run();Testing
rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_point_distance() {
let p1 = Point::new(0, 0);
let p2 = Point::new(3, 4);
assert_eq!(p1.distance_from(&p2), 5.0);
}
#[test]
fn test_counter() {
let mut counter = Counter::new(0, 5);
counter.increment();
assert_eq!(counter.get_count(), 5);
counter.decrement();
assert_eq!(counter.get_count(), 0);
}
}
// WASM-specific tests
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn test_wasm_add() {
assert_eq!(add(10, 20), 30);
}Best Practices
- 1Minimize JS-Wasm Boundaries
- Batch operations in Rust - Return complex objects instead of multiple calls
- 1Use Appropriate Types
- Prefer i32/f32 over i64/f64 (faster) - Use &[u8] for binary data
- 1Error Handling
- Use Result<T, JsValue> for fallible operations - Provide clear error messages
- 1Memory Management
- Reuse buffers when possible - Use linear memory for large data
- 1Testing
- Write unit tests in Rust - Use wasm-bindgen-test for browser tests
Production Checklist
- [ ] Release mode build (
--release) - [ ] Enable LTO in Cargo.toml
- [ ] Run wasm-opt for size optimization
- [ ] Test in target browsers
- [ ] Measure performance
- [ ] Check memory usage
- [ ] Validate TypeScript definitions
- [ ] Document public API
Rust and WebAssembly provide a powerful combination for building high-performance web applications. Start simple, optimize based on measurements, and enjoy the safety and speed of Rust!