Introduction to WebAssembly
WebAssembly (WASM) is a binary instruction format that runs at near-native speed in web browsers. Rust is one of the best languages for WASM development, offering both safety and performance. With tools like wasm-bindgen and wasm-pack, building WASM applications is straightforward.
- Near-native performance in the browser
- Small binary sizes (after optimization)
- Type safety and memory safety
- Great tooling (wasm-bindgen, wasm-pack)
- Easy JavaScript interop
1. Getting Started with wasm-bindgen
Project Setup
[package]
name = "wasm-example"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
"console",
"Document",
"Element",
"HtmlElement",
"Node",
"Window",
] }
[dev-dependencies]
wasm-bindgen-test = "0.3"
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Enable Link Time Optimization
codegen-units = 1 # Reduce parallel codegen for smaller binary
Basic WASM Module
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
let message = format!("Hello, {}!", name);
alert(&message);
}
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
log(&format!("Computing {} + {}", a, b));
a + b
}
#[wasm_bindgen(start)]
pub fn main() {
log("WASM module initialized!");
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rust WASM Example</title>
</head>
<body>
<h1>Rust WebAssembly Demo</h1>
<button onclick="runWasm()">Run WASM</button>
<script type="module">
import init, { greet, add } from './pkg/wasm_example.js';
async function run() {
await init();
window.runWasm = () => {
greet("World");
const result = add(5, 7);
console.log("5 + 7 =", result);
};
}
run();
</script>
</body>
</html>
Building and Running
# Install wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Build for web
wasm-pack build --target web
# Build for Node.js
wasm-pack build --target nodejs
# Build for bundler (webpack, rollup)
wasm-pack build --target bundler
# Serve locally
python3 -m http.server 8080
2. DOM Manipulation
use wasm_bindgen::prelude::*;
use web_sys::{Document, Element, HtmlElement, Window};
#[wasm_bindgen]
pub struct App {
document: Document,
}
#[wasm_bindgen]
impl App {
#[wasm_bindgen(constructor)]
pub fn new() -> Result {
let window = web_sys::window().expect("no global window");
let document = window.document().expect("no document");
Ok(App { document })
}
pub fn create_element(&self, tag: &str, text: &str) -> Result<(), JsValue> {
let element = self.document.create_element(tag)?;
element.set_text_content(Some(text));
let body = self.document.body().expect("no body");
body.append_child(&element)?;
Ok(())
}
pub fn set_title(&self, title: &str) {
self.document.set_title(title);
}
pub fn create_button(&self, id: &str, text: &str) -> Result<(), JsValue> {
let button = self.document
.create_element("button")?
.dyn_into::()?;
button.set_id(id);
button.set_inner_text(text);
let closure = Closure::wrap(Box::new(move || {
web_sys::console::log_1(&"Button clicked!".into());
}) as Box);
button.set_onclick(Some(closure.as_ref().unchecked_ref()));
closure.forget(); // Keep closure alive
self.document.body()
.expect("no body")
.append_child(&button)?;
Ok(())
}
}
#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {
let app = App::new()?;
app.set_title("Rust WASM App");
app.create_element("h1", "Hello from Rust!")?;
app.create_button("my-button", "Click me!")?;
Ok(())
}
3. Canvas Graphics
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
use std::cell::RefCell;
use std::rc::Rc;
#[wasm_bindgen]
pub struct Animation {
canvas: HtmlCanvasElement,
context: CanvasRenderingContext2d,
x: f64,
y: f64,
dx: f64,
dy: f64,
}
#[wasm_bindgen]
impl Animation {
#[wasm_bindgen(constructor)]
pub fn new(canvas_id: &str) -> Result {
let document = web_sys::window()
.unwrap()
.document()
.unwrap();
let canvas = document
.get_element_by_id(canvas_id)
.unwrap()
.dyn_into::()?;
let context = canvas
.get_context("2d")?
.unwrap()
.dyn_into::()?;
Ok(Animation {
canvas,
context,
x: 50.0,
y: 50.0,
dx: 2.0,
dy: 2.0,
})
}
pub fn update(&mut self) {
let width = self.canvas.width() as f64;
let height = self.canvas.height() as f64;
self.x += self.dx;
self.y += self.dy;
// Bounce off walls
if self.x <= 0.0 || self.x >= width {
self.dx = -self.dx;
}
if self.y <= 0.0 || self.y >= height {
self.dy = -self.dy;
}
}
pub fn draw(&self) {
let width = self.canvas.width() as f64;
let height = self.canvas.height() as f64;
// Clear canvas
self.context.clear_rect(0.0, 0.0, width, height);
// Draw circle
self.context.begin_path();
self.context.arc(self.x, self.y, 20.0, 0.0, 2.0 * std::f64::consts::PI)
.unwrap();
self.context.set_fill_style(&JsValue::from_str("#F39C12"));
self.context.fill();
}
}
fn request_animation_frame(f: &Closure) {
web_sys::window()
.unwrap()
.request_animation_frame(f.as_ref().unchecked_ref())
.unwrap();
}
#[wasm_bindgen(start)]
pub fn start_animation() -> Result<(), JsValue> {
let animation = Rc::new(RefCell::new(Animation::new("canvas")?));
let f = Rc::new(RefCell::new(None));
let g = f.clone();
let anim = animation.clone();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let mut anim = anim.borrow_mut();
anim.update();
anim.draw();
request_animation_frame(f.borrow().as_ref().unwrap());
}) as Box));
request_animation_frame(g.borrow().as_ref().unwrap());
Ok(())
}
4. Performance Optimization
Memory Management
use wasm_bindgen::prelude::*;
use js_sys::{Float32Array, Uint8Array};
#[wasm_bindgen]
pub struct ImageProcessor {
width: u32,
height: u32,
data: Vec,
}
#[wasm_bindgen]
impl ImageProcessor {
#[wasm_bindgen(constructor)]
pub fn new(width: u32, height: u32) -> ImageProcessor {
ImageProcessor {
width,
height,
data: vec![0; (width * height * 4) as usize],
}
}
// Zero-copy data transfer
pub fn get_data_ptr(&self) -> *const u8 {
self.data.as_ptr()
}
pub fn get_data_len(&self) -> usize {
self.data.len()
}
// Process image data in-place
pub fn grayscale(&mut self) {
for i in (0..self.data.len()).step_by(4) {
let r = self.data[i] as f32;
let g = self.data[i + 1] as f32;
let b = self.data[i + 2] as f32;
let gray = (r * 0.299 + g * 0.587 + b * 0.114) as u8;
self.data[i] = gray;
self.data[i + 1] = gray;
self.data[i + 2] = gray;
}
}
pub fn invert(&mut self) {
for i in (0..self.data.len()).step_by(4) {
self.data[i] = 255 - self.data[i];
self.data[i + 1] = 255 - self.data[i + 1];
self.data[i + 2] = 255 - self.data[i + 2];
}
}
// Blur effect
pub fn blur(&mut self, radius: u32) {
let mut temp = self.data.clone();
for y in 0..self.height {
for x in 0..self.width {
let mut r_sum = 0u32;
let mut g_sum = 0u32;
let mut b_sum = 0u32;
let mut count = 0u32;
for dy in -(radius as i32)..=(radius as i32) {
for dx in -(radius as i32)..=(radius as i32) {
let nx = (x as i32 + dx).max(0).min(self.width as i32 - 1) as u32;
let ny = (y as i32 + dy).max(0).min(self.height as i32 - 1) as u32;
let idx = ((ny * self.width + nx) * 4) as usize;
r_sum += self.data[idx] as u32;
g_sum += self.data[idx + 1] as u32;
b_sum += self.data[idx + 2] as u32;
count += 1;
}
}
let idx = ((y * self.width + x) * 4) as usize;
temp[idx] = (r_sum / count) as u8;
temp[idx + 1] = (g_sum / count) as u8;
temp[idx + 2] = (b_sum / count) as u8;
}
}
self.data = temp;
}
}
5. WebAssembly Threads
use wasm_bindgen::prelude::*;
use rayon::prelude::*;
#[wasm_bindgen]
pub fn parallel_sum(data: Vec) -> f32 {
data.par_iter().sum()
}
#[wasm_bindgen]
pub fn parallel_mandelbrot(
width: usize,
height: usize,
max_iter: u32,
) -> Vec {
let mut image = vec![0u8; width * height * 4];
image.par_chunks_mut(4).enumerate().for_each(|(i, pixel)| {
let x = i % width;
let y = i / width;
let cx = (x as f64 / width as f64) * 3.5 - 2.5;
let cy = (y as f64 / height as f64) * 2.0 - 1.0;
let mut zx = 0.0;
let mut zy = 0.0;
let mut iter = 0;
while zx * zx + zy * zy < 4.0 && iter < max_iter {
let tmp = zx * zx - zy * zy + cx;
zy = 2.0 * zx * zy + cy;
zx = tmp;
iter += 1;
}
let color = if iter == max_iter {
0
} else {
((iter as f64 / max_iter as f64) * 255.0) as u8
};
pixel[0] = color;
pixel[1] = color;
pixel[2] = color;
pixel[3] = 255;
});
image
}
6. Real-World Application: Game Engine
use wasm_bindgen::prelude::*;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, KeyboardEvent};
use std::cell::RefCell;
use std::rc::Rc;
#[wasm_bindgen]
pub struct Game {
canvas: HtmlCanvasElement,
context: CanvasRenderingContext2d,
player_x: f64,
player_y: f64,
enemies: Vec,
score: u32,
}
struct Enemy {
x: f64,
y: f64,
speed: f64,
}
#[wasm_bindgen]
impl Game {
#[wasm_bindgen(constructor)]
pub fn new() -> Result {
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document
.get_element_by_id("game-canvas")
.unwrap()
.dyn_into::()?;
let context = canvas
.get_context("2d")?
.unwrap()
.dyn_into::()?;
Ok(Game {
canvas,
context,
player_x: 400.0,
player_y: 300.0,
enemies: Vec::new(),
score: 0,
})
}
pub fn spawn_enemy(&mut self) {
use js_sys::Math;
self.enemies.push(Enemy {
x: Math::random() * self.canvas.width() as f64,
y: -20.0,
speed: 2.0 + Math::random() * 3.0,
});
}
pub fn update(&mut self, delta_time: f64) {
// Update enemies
self.enemies.retain_mut(|enemy| {
enemy.y += enemy.speed * delta_time;
// Check collision with player
let dx = enemy.x - self.player_x;
let dy = enemy.y - self.player_y;
let distance = (dx * dx + dy * dy).sqrt();
if distance < 30.0 {
self.score += 10;
return false;
}
enemy.y < self.canvas.height() as f64
});
// Spawn new enemies
if js_sys::Math::random() < 0.02 {
self.spawn_enemy();
}
}
pub fn draw(&self) {
let width = self.canvas.width() as f64;
let height = self.canvas.height() as f64;
// Clear
self.context.set_fill_style(&JsValue::from_str("#000"));
self.context.fill_rect(0.0, 0.0, width, height);
// Draw player
self.context.begin_path();
self.context.arc(
self.player_x,
self.player_y,
20.0,
0.0,
2.0 * std::f64::consts::PI,
).unwrap();
self.context.set_fill_style(&JsValue::from_str("#0f0"));
self.context.fill();
// Draw enemies
for enemy in &self.enemies {
self.context.begin_path();
self.context.arc(
enemy.x,
enemy.y,
15.0,
0.0,
2.0 * std::f64::consts::PI,
).unwrap();
self.context.set_fill_style(&JsValue::from_str("#f00"));
self.context.fill();
}
// Draw score
self.context.set_fill_style(&JsValue::from_str("#fff"));
self.context.set_font("24px Arial");
self.context
.fill_text(&format!("Score: {}", self.score), 10.0, 30.0)
.unwrap();
}
pub fn move_player(&mut self, dx: f64, dy: f64) {
self.player_x = (self.player_x + dx)
.max(20.0)
.min(self.canvas.width() as f64 - 20.0);
self.player_y = (self.player_y + dy)
.max(20.0)
.min(self.canvas.height() as f64 - 20.0);
}
}
Best Practices
🔑 WASM Optimization Tips
- Minimize String Copies: Use borrowed strings when possible
- Batch Operations: Reduce JS/WASM boundary crossings
- Use TypedArrays: Zero-copy data transfer
- Profile: Use browser DevTools to find bottlenecks
- Optimize Build: Use release mode with LTO
- Consider Size: Use wasm-opt for smaller binaries
Summary
✅ You've learned:
- Building WASM modules with wasm-bindgen
- DOM manipulation from Rust
- Canvas graphics and animation
- Performance optimization techniques
- Multi-threaded WASM with Rayon
- Building interactive web applications
WebAssembly opens up new possibilities for web development, bringing near-native performance to the browser. Rust's strong type system and memory safety make it an ideal choice for WASM development. You're now equipped to build high-performance web applications that leverage the best of both Rust and the web platform!