Skip to main content
Technology & EngineeringWebassembly274 lines

Rust WASM

Compiling Rust to WebAssembly using wasm-pack, wasm-bindgen, and the Rust Wasm ecosystem

Quick Summary18 lines
You are an expert in compiling Rust to WebAssembly for building WebAssembly applications.

## Key Points

- **`wasm-bindgen`** — core bindings between Rust and JS
- **`js-sys`** — bindings to standard JavaScript built-in objects
- **`web-sys`** — bindings to Web APIs (DOM, fetch, canvas, etc.)
- **`gloo`** — higher-level ergonomic wrappers for web APIs
- **`serde-wasm-bindgen`** — serialize/deserialize between Rust and JS values
- **Set `crate-type = ["cdylib"]`** — this is required for producing `.wasm` output. Include `"rlib"` too if you need to run Rust tests.
- **Use `#[wasm_bindgen(start)]`** for initialization — this runs a function automatically when the module is instantiated.
- **Enable `wee_alloc` or `lol_alloc` for smaller binaries** — the default allocator adds significant size; smaller allocators trade performance for size.
- **Run `wasm-opt -Oz`** on release builds — `wasm-pack build --release` does this automatically, shrinking binaries by 10-30%.
- **Use `console_error_panic_hook`** — install it early to get readable panic messages in the browser console instead of `unreachable` errors.
- **Call `.free()` on Rust structs in JS** — Wasm objects are not garbage collected. Leaking them causes memory to grow indefinitely.
- **Batch memory access** — pass slices or typed arrays instead of individual values to reduce boundary crossings.
skilldb get webassembly-skills/Rust WASMFull skill: 274 lines
Paste into your CLAUDE.md or agent config

Rust to Wasm — WebAssembly

You are an expert in compiling Rust to WebAssembly for building WebAssembly applications.

Overview

Rust is one of the best-supported languages for WebAssembly compilation. The wasm32-unknown-unknown target produces compact, high-performance Wasm binaries. The wasm-pack toolchain handles building, binding generation, and npm packaging, while wasm-bindgen provides seamless interop with JavaScript types and browser APIs.

Core Concepts

Compilation Targets

TargetUse Case
wasm32-unknown-unknownBrowser and generic Wasm (no OS/libc)
wasm32-wasip1WASI-compatible server-side Wasm
wasm32-unknown-emscriptenEmscripten-based builds (legacy)

wasm-pack

wasm-pack is the primary build tool. It compiles Rust to Wasm, runs wasm-bindgen, optimizes with wasm-opt, and generates an npm-ready package.

wasm-bindgen

wasm-bindgen generates the JavaScript glue code that bridges Rust types and JavaScript. It handles converting strings, structs, enums, and closures across the Wasm boundary.

Key Crates

  • wasm-bindgen — core bindings between Rust and JS
  • js-sys — bindings to standard JavaScript built-in objects
  • web-sys — bindings to Web APIs (DOM, fetch, canvas, etc.)
  • gloo — higher-level ergonomic wrappers for web APIs
  • serde-wasm-bindgen — serialize/deserialize between Rust and JS values

Implementation Patterns

Project Setup

# Install prerequisites
cargo install wasm-pack
rustup target add wasm32-unknown-unknown

# Create a new library project
cargo init --lib my-wasm-lib
# Cargo.toml
[package]
name = "my-wasm-lib"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
serde = { version = "1", features = ["derive"] }
serde-wasm-bindgen = "0.6"

[dependencies.web-sys]
version = "0.3"
features = ["Document", "Element", "HtmlElement", "Window", "console"]

Basic Exported Function

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => {
            let (mut a, mut b) = (0u32, 1u32);
            for _ in 2..=n {
                let temp = b;
                b = a + b;
                a = temp;
            }
            b
        }
    }
}

Exporting Structs

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct Particle {
    x: f64,
    y: f64,
    vx: f64,
    vy: f64,
}

#[wasm_bindgen]
impl Particle {
    #[wasm_bindgen(constructor)]
    pub fn new(x: f64, y: f64) -> Particle {
        Particle { x, y, vx: 0.0, vy: 0.0 }
    }

    pub fn apply_force(&mut self, fx: f64, fy: f64) {
        self.vx += fx;
        self.vy += fy;
    }

    pub fn step(&mut self, dt: f64) {
        self.x += self.vx * dt;
        self.y += self.vy * dt;
    }

    #[wasm_bindgen(getter)]
    pub fn x(&self) -> f64 { self.x }

    #[wasm_bindgen(getter)]
    pub fn y(&self) -> f64 { self.y }
}

Calling JavaScript from Rust

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);

    #[wasm_bindgen(js_namespace = Math)]
    fn random() -> f64;

    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn run() {
    log("Running from Rust!");
    let r = random();
    log(&format!("Random number: {}", r));
}

DOM Manipulation with web-sys

use wasm_bindgen::prelude::*;
use web_sys::{Document, Element, Window};

#[wasm_bindgen]
pub fn create_element(tag: &str, text: &str) -> Result<Element, JsValue> {
    let window: Window = web_sys::window().ok_or("no window")?;
    let document: Document = window.document().ok_or("no document")?;
    let element = document.create_element(tag)?;
    element.set_text_content(Some(text));
    let body = document.body().ok_or("no body")?;
    body.append_child(&element)?;
    Ok(element)
}

Building and Using

# Build for bundlers (webpack, vite, etc.)
wasm-pack build --target bundler

# Build for direct browser use (no bundler)
wasm-pack build --target web

# Build for Node.js
wasm-pack build --target nodejs

# Optimized release build
wasm-pack build --release
// Using in JavaScript (bundler target)
import init, { greet, Particle } from './pkg/my_wasm_lib.js';

await init();
console.log(greet('World'));

const p = new Particle(0, 0);
p.apply_force(1.0, 0.5);
p.step(0.016);
console.log(p.x, p.y);
p.free(); // manually free Wasm memory

Passing Complex Data with Serde

use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;

#[derive(Serialize, Deserialize)]
pub struct Config {
    pub width: u32,
    pub height: u32,
    pub title: String,
}

#[wasm_bindgen]
pub fn process_config(val: JsValue) -> Result<JsValue, JsValue> {
    let config: Config = serde_wasm_bindgen::from_value(val)?;
    let result = Config {
        title: format!("Processed: {}", config.title),
        ..config
    };
    Ok(serde_wasm_bindgen::to_value(&result)?)
}

Best Practices

  • Set crate-type = ["cdylib"] — this is required for producing .wasm output. Include "rlib" too if you need to run Rust tests.
  • Use #[wasm_bindgen(start)] for initialization — this runs a function automatically when the module is instantiated.
  • Enable wee_alloc or lol_alloc for smaller binaries — the default allocator adds significant size; smaller allocators trade performance for size.
  • Run wasm-opt -Oz on release builds — wasm-pack build --release does this automatically, shrinking binaries by 10-30%.
  • Use console_error_panic_hook — install it early to get readable panic messages in the browser console instead of unreachable errors.
  • Call .free() on Rust structs in JS — Wasm objects are not garbage collected. Leaking them causes memory to grow indefinitely.
  • Batch memory access — pass slices or typed arrays instead of individual values to reduce boundary crossings.

Common Pitfalls

  • Forgetting --target web for non-bundler use — the default target assumes a bundler. Loading the output directly in a browser <script> tag without a bundler will fail unless you use --target web.
  • String ownership confusion&str parameters are borrowed from JS and only valid during the call. Storing them requires cloning to a String.
  • Not calling .free() — Rust-allocated objects on the Wasm heap are never garbage collected by JavaScript. Every new MyStruct() in JS needs a corresponding .free().
  • Missing web-sys features — each Web API type must be enabled in Cargo.toml features. Compilation errors about missing methods usually mean a feature flag is needed.
  • Panics become RuntimeError: unreachable — without console_error_panic_hook, Rust panics produce cryptic errors with no stack trace or message.
  • Large binary sizes in debug mode — debug builds can be 5-10x larger than release. Always profile and ship release builds.

Core Philosophy

Rust's ownership model and zero-cost abstractions make it an ideal source language for WebAssembly. You get memory safety without garbage collection, predictable performance without runtime overhead, and a type system that catches errors at compile time rather than at runtime in the user's browser. Lean into Rust's strengths: use strong types, leverage the borrow checker, and let the compiler eliminate entire classes of bugs before the code reaches Wasm.

wasm-bindgen is the bridge that makes Rust-to-Wasm practical for web development. It handles the tedious work of converting between Rust types and JavaScript values, generating the glue code that connects your Rust functions to the browser. Use it for everything that crosses the boundary — strings, structs, closures, and DOM interactions — rather than building your own marshaling layer.

Binary size is your deployment tax. Every kilobyte of Wasm must be downloaded, compiled, and instantiated before your code can run. Use opt-level = "z", enable LTO, switch to a small allocator like lol_alloc, and run wasm-opt -Oz on the output. A Rust Wasm module that is 50KB gzipped loads in milliseconds; one that is 2MB loads in seconds. The performance advantage of Wasm is meaningless if users wait for the binary to arrive.

Anti-Patterns

  • Not calling .free() on Rust structs in JavaScript — Wasm-allocated objects are never garbage collected by the JavaScript engine; every new MyStruct() created via wasm-bindgen must have a corresponding .free() call or memory leaks indefinitely.

  • Using --target bundler and loading without a bundler — the default wasm-pack target assumes webpack or similar; loading the output directly in a browser <script> tag requires --target web.

  • Panicking without console_error_panic_hook — without this crate, Rust panics produce a cryptic RuntimeError: unreachable with no message or stack trace, making debugging nearly impossible.

  • Exporting every function — each #[wasm_bindgen] export prevents dead-code elimination for that function and its dependencies; only export what the JavaScript consumer actually needs.

  • Forgetting to enable web-sys features — each Web API type must be explicitly enabled in Cargo.toml; missing feature flags produce confusing compilation errors about missing methods on types that clearly exist in the documentation.

Install this skill directly: skilldb add webassembly-skills

Get CLI access →