Introduction
WebAssembly (WASM) is a bytecode format that runs at near-native speeds in browsers and servers. Rust is one of the best languages for WebAssembly development, offering memory safety and zero-cost abstractions.
This guide covers building WebAssembly applications with Rust for multiple targets.
Why Rust + WebAssembly?
Performance Comparison
Browser Runtime Performance (Matrix multiplication 1000x1000):
Language/Runtime Time Memory Speed vs JS
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
JavaScript 850ms 150MB 1x
Rust โ WASM 45ms 20MB 19x faster
Python (js) 4500ms 500MB 0.19x
Go โ WASM 60ms 50MB 14x faster
C โ WASM 40ms 10MB 21x faster
Key Advantages
โ
Near-native performance (80-90% of native)
โ
Memory safety guarantees
โ
Smaller bundle sizes than JavaScript
โ
Can share code between browser and server
โ
Excellent tooling (wasm-pack, wasm-bindgen)
โ
Growing ecosystem
Setup and Tooling
Installation
# Install Rust
rustup update
# Install WASM target
rustup target add wasm32-unknown-unknown
# Install wasm-pack (build tool)
curl https://rustwasm.org/wasm-pack/installer/init.sh -sSf | sh
# Install necessary tools
cargo install wasm-pack
Create Project
# Create new WASM library
wasm-pack new my_wasm_app
# Or convert existing Rust project
cargo init my_project
# Add to Cargo.toml:
# [lib]
# crate-type = ["cdylib"]
Hello World in WASM
Rust Code
// src/lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[wasm_bindgen]
pub struct Counter {
count: i32,
}
#[wasm_bindgen]
impl Counter {
#[wasm_bindgen(constructor)]
pub fn new() -> Counter {
Counter { count: 0 }
}
pub fn increment(&mut self) {
self.count += 1;
}
pub fn get_count(&self) -> i32 {
self.count
}
}
Cargo.toml
[package]
name = "hello_wasm"
version = "0.1.0"
edition = "2021"
[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[lib]
crate-type = ["cdylib"]
Build
wasm-pack build --target web
# Output:
# pkg/
# โโโ hello_wasm.js
# โโโ hello_wasm.d.ts
# โโโ hello_wasm_bg.wasm (binary)
# โโโ package.json
JavaScript Usage
// Import the WASM module
import init, { greet, Counter } from './pkg/hello_wasm.js';
async function main() {
await init();
// Call Rust function
console.log(greet("World")); // "Hello, World!"
// Use Rust struct
const counter = new Counter();
counter.increment();
counter.increment();
console.log(counter.get_count()); // 2
}
main();
DOM Manipulation from Rust
web-sys Crate
use wasm_bindgen::prelude::*;
use web_sys::{Document, HtmlElement, Window};
#[wasm_bindgen]
pub fn manipulate_dom() -> Result<(), JsValue> {
// Get window and document
let window = web_sys::window()
.ok_or("No window")?;
let document = window.document()
.ok_or("No document")?;
// Create element
let div = document.create_element("div")?;
div.set_inner_html("Hello from Rust!");
// Append to body
let body = document.body()
.ok_or("No body")?;
body.append_child(&div)?;
Ok(())
}
Event Handling
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::HtmlButtonElement;
#[wasm_bindgen]
pub fn setup_click_handler() -> Result<(), JsValue> {
let document = web_sys::window()
.unwrap()
.document()
.unwrap();
let button = document
.get_element_by_id("my-button")
.ok_or("No button")?
.dyn_into::<HtmlButtonElement>()?;
let closure = Closure::new(|| {
web_sys::console::log_1(&"Button clicked!".into());
});
button.set_onclick(Some(closure.as_ref().unchecked_ref()));
closure.forget();
Ok(())
}
Performance-Critical Code
Image Processing Example
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct ImageProcessor {
data: Vec<u8>,
width: u32,
height: u32,
}
#[wasm_bindgen]
impl ImageProcessor {
#[wasm_bindgen(constructor)]
pub fn new(width: u32, height: u32) -> ImageProcessor {
ImageProcessor {
data: vec![0; (width * height * 4) as usize],
width,
height,
}
}
// Apply grayscale filter
pub fn grayscale(&mut self) {
for pixel in self.data.chunks_exact_mut(4) {
let r = pixel[0] as f32;
let g = pixel[1] as f32;
let b = pixel[2] as f32;
let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
pixel[0] = gray;
pixel[1] = gray;
pixel[2] = gray;
}
}
// Get pixel data
pub fn data_ptr(&self) -> *const u8 {
self.data.as_ptr()
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
}
Server-Side WASM
WASM on Node.js
// src/lib.rs - Same Rust code works on server!
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
Node.js Setup
# Cargo.toml
[lib]
crate-type = ["cdylib"]
# Build for Node.js
wasm-pack build --target nodejs
// JavaScript using Node.js WASM
const { fibonacci } = require('./pkg/my_wasm_bg.js');
console.log(fibonacci(10)); // Fast calculation!
// Export as CommonJS module
module.exports = { fibonacci };
Interop: Passing Complex Data
Serialization
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Point {
pub x: f64,
pub y: f64,
}
#[wasm_bindgen]
pub fn distance(p1: &JsValue, p2: &JsValue) -> f64 {
let p1: Point = p1.into_serde().unwrap();
let p2: Point = p2.into_serde().unwrap();
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
(dx * dx + dy * dy).sqrt()
}
JavaScript
import { distance } from './pkg/my_wasm.js';
const p1 = { x: 0, y: 0 };
const p2 = { x: 3, y: 4 };
console.log(distance(p1, p2)); // 5.0
Memory Management
Shared Memory
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct Buffer {
data: Vec<u8>,
}
#[wasm_bindgen]
impl Buffer {
#[wasm_bindgen(constructor)]
pub fn new(size: usize) -> Buffer {
Buffer {
data: vec![0; size],
}
}
// Get mutable pointer for direct access
pub fn as_mut_ptr(&mut self) -> *mut u8 {
self.data.as_mut_ptr()
}
// Get immutable pointer
pub fn as_ptr(&self) -> *const u8 {
self.data.as_ptr()
}
pub fn len(&self) -> usize {
self.data.len()
}
}
JavaScript Memory Access
import { Buffer } from './pkg/buffer.js';
const buffer = new Buffer(1024);
const ptr = buffer.as_ptr();
// Create JavaScript view of WASM memory
const memory = buffer.memory.buffer;
const view = new Uint8Array(memory, ptr, 1024);
// Direct write
view[0] = 42;
view[1] = 100;
Real-World Example: Mandelbrot Set
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn compute_mandelbrot(
width: u32,
height: u32,
max_iter: u32,
) -> Vec<u8> {
let mut result = vec![0u8; (width * height) as usize];
for y in 0..height {
for x in 0..width {
let cx = -2.5 + (x as f64 / width as f64) * 3.5;
let cy = -1.25 + (y as f64 / height as f64) * 2.5;
let mut zx = 0.0;
let mut zy = 0.0;
let mut iter = 0;
while iter < max_iter && zx * zx + zy * zy <= 4.0 {
let tx = zx * zx - zy * zy + cx;
zy = 2.0 * zx * zy + cy;
zx = tx;
iter += 1;
}
result[(y * width + x) as usize] = (iter % 256) as u8;
}
}
result
}
JavaScript Renderer
import { compute_mandelbrot } from './pkg/mandelbrot.js';
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Compute in Rust (fast!)
const data = compute_mandelbrot(800, 600, 100);
// Render in JavaScript
const imageData = ctx.createImageData(800, 600);
for (let i = 0; i < data.length; i++) {
imageData.data[i * 4] = data[i]; // R
imageData.data[i * 4 + 1] = data[i]; // G
imageData.data[i * 4 + 2] = data[i]; // B
imageData.data[i * 4 + 3] = 255; // A
}
ctx.putImageData(imageData, 0, 0);
Debugging
console API
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn debug_example() {
web_sys::console::log_1(&"Debug message".into());
web_sys::console::warn_1(&"Warning!".into());
web_sys::console::error_1(&"Error!".into());
}
Browser DevTools
# Build with source maps
wasm-pack build --dev
# In Firefox:
# about:debugging โ Components โ WebAssembly
# Can set breakpoints and step through code!
Optimization Tips
Performance optimizations:
โ
Use --release builds for production
โ
Enable LTO: lto = true in Cargo.toml
โ
Minimize allocations in hot paths
โ
Use SIMD when available
โ
Profile with Web Performance API
โ
Keep WASM modules < 1MB
โ
Use streaming compilation
Bundle Size Optimization
Typical sizes:
Code Size Compressed
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Hello world 50KB 15KB
Image processor 200KB 60KB
Game engine 5MB 1.5MB
Optimization techniques:
- Use wee_alloc for smaller allocator
- Remove debug symbols
- Use cargo-bloat to find large functions
- Enable link-time optimization (LTO)
Glossary
- WASM: WebAssembly - bytecode format
- wasm-bindgen: Glue code between JS and Rust
- wasm-pack: Build tool for WASM projects
- cdylib: Dynamic library crate type
- JsValue: JavaScript value from Rust
Comments