WebAssembly (Wasm) enables running Rust code in browsers at near-native speed. This opens possibilities for compute-intensive tasks like games, image processing, and cryptography that JavaScript can't handle efficiently. This guide covers building Wasm modules with Rust from a senior developer's perspective.
Why Rust + WebAssembly
The combination excels because:
- Performance: Near-native speed in browsers
- Safety: Rust's guarantees carry over to Wasm
- Size: Smaller binaries than C/C++
- Interop: Seamless JavaScript integration
- Portability: Runs anywhere Wasm runs
Use cases: Games, audio/video processing, cryptography, simulations, parsers, and any CPU-intensive work.
Setup
Install Rust
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Add Wasm target
rustup target add wasm32-unknown-unknown
# Install wasm-pack (recommended)
cargo install wasm-pack
# Install wasm-bindgen-cli (for manual builds)
cargo install wasm-bindgen-cli
Project Structure
# Create a new project
cargo new --lib my-wasm
cd my-wasm
my-wasm/
├── Cargo.toml
├── src/
│ └── lib.rs
├── pkg/ # Generated by wasm-pack
│ ├── my_wasm.js
│ ├── my_wasm_bg.wasm
│ └── package.json
└── www/ # Optional: web demo
└── index.html
Configuration
Cargo.toml
[package]
name = "my-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3" # JavaScript types
web-sys = "0.3" # Web APIs
# For console.log debugging
console_error_panic_hook = "0.1"
[dev-dependencies]
wasm-bindgen-test = "0.3"
[profile.release]
# Optimize for size
opt-level = "s"
lto = true
Basic Example
Simple Function
src/lib.rs:
use wasm_bindgen::prelude::*;
// Called when the Wasm module is instantiated
#[wasm_bindgen(start)]
pub fn main() {
// Set panic hook for better error messages
console_error_panic_hook::set_once();
}
// Export a simple function
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// Export a function that takes and returns numbers
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// Export a function that works with arrays
#[wasm_bindgen]
pub fn sum(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}
Build
# Build for web (recommended)
wasm-pack build --target web
# Build for bundlers (webpack, etc.)
wasm-pack build --target bundler
# Build for Node.js
wasm-pack build --target nodejs
# Release build with optimizations
wasm-pack build --target web --release
Use in HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Rust Wasm Demo</title>
</head>
<body>
<h1>Rust WebAssembly</h1>
<div id="output"></div>
<script type="module">
import init, { greet, add, sum } from './pkg/my_wasm.js';
async function run() {
// Initialize the Wasm module
await init();
// Call Rust functions
const greeting = greet("World");
console.log(greeting); // "Hello, World!"
const result = add(5, 3);
console.log("5 + 3 =", result); // 8
const total = sum(new Int32Array([1, 2, 3, 4, 5]));
console.log("Sum:", total); // 15
document.getElementById('output').textContent = greeting;
}
run();
</script>
</body>
</html>
JavaScript Interop
Call JavaScript from Rust
use wasm_bindgen::prelude::*;
// Import JavaScript functions
#[wasm_bindgen]
extern "C" {
// console.log
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
// alert
fn alert(s: &str);
// Custom JavaScript function
#[wasm_bindgen(js_namespace = window)]
fn customCallback(value: i32);
}
#[wasm_bindgen]
pub fn rust_function() {
log("Called from Rust!");
alert("Hello from Rust!");
}
Work with the DOM
Enable web-sys features in Cargo.toml:
[dependencies.web-sys]
version = "0.3"
features = [
"Document",
"Element",
"HtmlElement",
"Window",
"console",
]
use wasm_bindgen::prelude::*;
use web_sys::{Document, Element, Window};
#[wasm_bindgen]
pub fn manipulate_dom() -> Result<(), JsValue> {
let window: Window = web_sys::window().expect("no window");
let document: Document = window.document().expect("no document");
// Create element
let element: Element = document.create_element("div")?;
element.set_inner_html("<h1>Created by Rust!</h1>");
element.set_attribute("class", "rust-created")?;
// Append to body
let body = document.body().expect("no body");
body.append_child(&element)?;
Ok(())
}
Callbacks and Closures
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn set_timeout_rust(callback: &js_sys::Function, delay: i32) {
let window = web_sys::window().expect("no window");
window.set_timeout_with_callback_and_timeout_and_arguments_0(
callback,
delay
).unwrap();
}
// Rust closure to JavaScript
#[wasm_bindgen]
pub fn create_callback() -> JsValue {
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
web_sys::console::log_1(&format!("Clicked at: {}, {}", event.client_x(), event.client_y()).into());
}) as Box<dyn FnMut(_)>);
let js_value = closure.as_ref().clone();
closure.forget(); // Prevent Rust from dropping the closure
js_value
}
Structs and Classes
Export Struct as Class
use wasm_bindgen::prelude::*;
#[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
}
pub fn set(&mut self, value: i32) {
self.value = value;
}
}
JavaScript usage:
import init, { Counter } from './pkg/my_wasm.js';
await init();
const counter = new Counter(10);
console.log(counter.get()); // 10
counter.increment();
counter.increment();
console.log(counter.get()); // 12
counter.free(); // Clean up Wasm memory
Practical Example: Image Processing
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn grayscale(data: &mut [u8]) {
// Process RGBA pixel data
for pixel in data.chunks_exact_mut(4) {
let r = pixel[0] as f32;
let g = pixel[1] as f32;
let b = pixel[2] as f32;
// Luminosity method
let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
pixel[0] = gray;
pixel[1] = gray;
pixel[2] = gray;
// pixel[3] is alpha; leave unchanged
}
}
#[wasm_bindgen]
pub fn invert(data: &mut [u8]) {
for pixel in data.chunks_exact_mut(4) {
pixel[0] = 255 - pixel[0];
pixel[1] = 255 - pixel[1];
pixel[2] = 255 - pixel[2];
}
}
#[wasm_bindgen]
pub fn brightness(data: &mut [u8], adjustment: i32) {
for pixel in data.chunks_exact_mut(4) {
pixel[0] = ((pixel[0] as i32 + adjustment).clamp(0, 255)) as u8;
pixel[1] = ((pixel[1] as i32 + adjustment).clamp(0, 255)) as u8;
pixel[2] = ((pixel[2] as i32 + adjustment).clamp(0, 255)) as u8;
}
}
JavaScript integration:
import init, { grayscale } from './pkg/my_wasm.js';
async function processImage() {
await init();
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Get image data
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Process with Rust
grayscale(imageData.data);
// Put processed data back
ctx.putImageData(imageData, 0, 0);
}
Testing
Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}
Wasm Tests
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn test_greet() {
assert_eq!(greet("Test"), "Hello, Test!");
}
#[wasm_bindgen_test]
fn test_in_browser() {
let window = web_sys::window().expect("no window");
assert!(window.location().href().is_ok());
}
Run tests:
# Run unit tests
cargo test
# Run Wasm tests in a headless browser
wasm-pack test --headless --chrome
wasm-pack test --headless --firefox
Optimization
Minimize Size
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Single codegen unit
panic = "abort" # Abort on panic
strip = true # Strip symbols
# Use wasm-opt for further optimization
wasm-pack build --release
# Or manually
wasm-opt -Oz pkg/my_wasm_bg.wasm -o pkg/my_wasm_bg.wasm
Measure Size
# Check file size
ls -lh pkg/*.wasm
# Analyze sections
wasm-objdump -h pkg/my_wasm_bg.wasm
Integration with Build Tools
Webpack
// webpack.config.js
module.exports = {
experiments: {
asyncWebAssembly: true
}
};
// In your app
import * as wasm from 'my-wasm';
wasm.greet("Webpack");
Vite
// vite.config.js
import wasm from 'vite-plugin-wasm';
export default {
plugins: [wasm()]
};
Key Takeaways
- Use wasm-pack: Simplifies build and packaging
- Start small: Export simple functions first
- Memory management: Call
.free() on Rust structs - Optimize for size: Release builds with proper settings
- Test in the browser: Some APIs only work in the browser
- Choose the right target: web, bundler, or nodejs
WebAssembly with Rust opens new possibilities for web performance—leverage it for compute-intensive tasks that would be too slow in JavaScript.