Chapter 7 ยท Level 8

๐ŸŒ WebAssembly

High-Performance Web Applications with Rust

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.

โ„น๏ธ Why Rust + WASM?
  • 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

๐Ÿ“ Cargo.toml
[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

๐Ÿ“ lib.rs - Hello WASM
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!");
}
๐Ÿ“ index.html - Using the WASM Module
<!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

๐Ÿ“ Build Commands
# 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

๐Ÿ“ Example: DOM Interaction
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

๐Ÿ“ Example: Canvas Animation
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

๐Ÿ“ Example: Efficient Memory Usage
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

๐Ÿ“ Example: Multi-threaded WASM
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

๐Ÿ“ Example: Simple Game Loop
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

  1. Minimize String Copies: Use borrowed strings when possible
  2. Batch Operations: Reduce JS/WASM boundary crossings
  3. Use TypedArrays: Zero-copy data transfer
  4. Profile: Use browser DevTools to find bottlenecks
  5. Optimize Build: Use release mode with LTO
  6. 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!