Rust WASM
Compiling Rust to WebAssembly using wasm-pack, wasm-bindgen, and the Rust Wasm ecosystem
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 linesRust 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
| Target | Use Case |
|---|---|
wasm32-unknown-unknown | Browser and generic Wasm (no OS/libc) |
wasm32-wasip1 | WASI-compatible server-side Wasm |
wasm32-unknown-emscripten | Emscripten-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 JSjs-sys— bindings to standard JavaScript built-in objectsweb-sys— bindings to Web APIs (DOM, fetch, canvas, etc.)gloo— higher-level ergonomic wrappers for web APIsserde-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.wasmoutput. 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_allocorlol_allocfor smaller binaries — the default allocator adds significant size; smaller allocators trade performance for size. - Run
wasm-opt -Ozon release builds —wasm-pack build --releasedoes 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 ofunreachableerrors. - 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 webfor 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 —
&strparameters are borrowed from JS and only valid during the call. Storing them requires cloning to aString. - Not calling
.free()— Rust-allocated objects on the Wasm heap are never garbage collected by JavaScript. Everynew MyStruct()in JS needs a corresponding.free(). - Missing web-sys features — each Web API type must be enabled in
Cargo.tomlfeatures. Compilation errors about missing methods usually mean a feature flag is needed. - Panics become
RuntimeError: unreachable— withoutconsole_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; everynew MyStruct()created via wasm-bindgen must have a corresponding.free()call or memory leaks indefinitely. -
Using
--target bundlerand 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 crypticRuntimeError: unreachablewith 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
Related Skills
Assemblyscript
Writing WebAssembly modules using AssemblyScript, a TypeScript-like language that compiles to Wasm
Js Interop
JavaScript and WebAssembly interop patterns including memory sharing, type marshaling, and binding generation
Performance
Optimizing WebAssembly performance including binary size, execution speed, memory usage, and profiling techniques
Wasi
WebAssembly System Interface (WASI) for portable system-level access including filesystem, networking, and clocks
WASM Basics
WebAssembly fundamentals including module structure, types, memory model, and binary/text formats
WASM in Browser
Using WebAssembly in the browser for canvas rendering, audio processing, Web Workers, and DOM integration