Rust WASM: WebAssembly with Rust
WebAssembly (WASM) is a binary instruction format that runs in modern browsers at near-native speed. It's not meant to replace JavaScript—it's a compilation target for languages like Rust, C++, and...
Key Insights
- Rust compiles to WebAssembly with zero runtime overhead, producing binaries 50-80% smaller than equivalent C++ code while guaranteeing memory safety at compile time
- The
wasm-bindgentool handles all the JavaScript interop boilerplate, letting you call DOM APIs from Rust and export Rust functions with automatic type conversions - WASM shines for CPU-intensive tasks like image processing or parsing, but the FFI boundary has overhead—don’t use it for simple DOM manipulation or functions called thousands of times per frame
Introduction to WebAssembly and Why Rust
WebAssembly (WASM) is a binary instruction format that runs in modern browsers at near-native speed. It’s not meant to replace JavaScript—it’s a compilation target for languages like Rust, C++, and Go that need predictable performance for computationally intensive tasks.
Rust is the best-in-class language for WASM development. Unlike C++, Rust guarantees memory safety without a garbage collector, eliminating entire classes of bugs. Unlike Go, Rust doesn’t bundle a runtime, producing binaries that are typically 200-500KB instead of several megabytes. The Rust compiler’s aggressive optimizations and zero-cost abstractions mean your WASM modules are both small and fast.
The Rust ecosystem has first-class WASM support through wasm-pack and wasm-bindgen, making the development experience significantly smoother than other languages. You get type-safe JavaScript interop, automatic memory management across the FFI boundary, and seamless integration with modern bundlers.
Setting Up the Rust WASM Toolchain
Install wasm-pack, which bundles everything you need:
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
Create a new library project:
cargo new --lib rust_wasm_demo
cd rust_wasm_demo
Configure Cargo.toml for WASM:
[package]
name = "rust_wasm_demo"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Enable link-time optimization
codegen-units = 1 # Maximize optimization
The cdylib crate type produces a dynamic library suitable for WASM. The release profile optimizations are crucial—they typically reduce binary size by 40-60%.
Building Your First Rust WASM Module
Let’s create a simple greeting function. Replace src/lib.rs:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}! Welcome to Rust WASM.", name)
}
#[wasm_bindgen]
pub fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}
The #[wasm_bindgen] attribute marks functions for JavaScript export. wasm-bindgen handles all the marshaling between Rust and JavaScript types automatically.
Build the project:
wasm-pack build --target web
This generates a pkg/ directory with your WASM module, JavaScript bindings, and TypeScript definitions. Use it in HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rust WASM Demo</title>
</head>
<body>
<script type="module">
import init, { greet, add_numbers } from './pkg/rust_wasm_demo.js';
async function run() {
await init();
console.log(greet('Developer'));
console.log('5 + 7 =', add_numbers(5, 7));
}
run();
</script>
</body>
</html>
The init() function loads and instantiates the WASM module. After it resolves, all exported Rust functions are available as JavaScript functions.
Interfacing Between Rust and JavaScript
wasm-bindgen enables bidirectional communication. You can import JavaScript functions into Rust and export complex Rust types to JavaScript.
Here’s Rust calling JavaScript DOM APIs:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
#[wasm_bindgen(js_namespace = document)]
fn getElementById(id: &str) -> Option<web_sys::Element>;
}
#[wasm_bindgen]
pub fn update_dom(element_id: &str, text: &str) {
log(&format!("Updating element: {}", element_id));
if let Some(element) = getElementById(element_id) {
element.set_inner_html(text);
}
}
For more comprehensive DOM access, add web-sys to Cargo.toml:
[dependencies]
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["Document", "Element", "HtmlElement", "Window"] }
Now you can manipulate the DOM type-safely from Rust:
use wasm_bindgen::prelude::*;
use web_sys::{Document, Element};
#[wasm_bindgen]
pub fn create_element_rust() -> Result<(), JsValue> {
let window = web_sys::window().expect("no global window");
let document = window.document().expect("no document");
let div = document.create_element("div")?;
div.set_inner_html("Created from Rust!");
div.set_class_name("rust-element");
document
.body()
.expect("no body")
.append_child(&div)?;
Ok(())
}
Performance-Critical Use Case
WASM’s real value appears in CPU-intensive operations. Here’s an image brightness filter comparing Rust and JavaScript:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn adjust_brightness(pixels: &mut [u8], factor: f32) {
for chunk in pixels.chunks_exact_mut(4) {
chunk[0] = (chunk[0] as f32 * factor).min(255.0) as u8; // R
chunk[1] = (chunk[1] as f32 * factor).min(255.0) as u8; // G
chunk[2] = (chunk[2] as f32 * factor).min(255.0) as u8; // B
// chunk[3] is alpha, leave unchanged
}
}
JavaScript equivalent:
function adjustBrightnessJS(pixels, factor) {
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = Math.min(pixels[i] * factor, 255); // R
pixels[i+1] = Math.min(pixels[i+1] * factor, 255); // G
pixels[i+2] = Math.min(pixels[i+2] * factor, 255); // B
}
}
Usage in JavaScript:
import init, { adjust_brightness } from './pkg/rust_wasm_demo.js';
async function processImage() {
await init();
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Rust WASM version
const start = performance.now();
adjust_brightness(imageData.data, 1.5);
console.log('Rust WASM:', performance.now() - start, 'ms');
ctx.putImageData(imageData, 0, 0);
}
For a 1920x1080 image, Rust WASM typically runs 3-5x faster than JavaScript. The gap widens with more complex algorithms because SIMD optimizations and predictable memory layouts give Rust a compounding advantage.
Building and Deploying
For production builds with bundlers, use --target bundler:
wasm-pack build --target bundler --release
Webpack configuration:
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
experiments: {
asyncWebAssembly: true,
},
mode: 'production',
};
Vite works out of the box with WASM:
// main.js
import init, { greet } from './pkg/rust_wasm_demo.js';
init().then(() => {
console.log(greet('Vite User'));
});
For maximum size optimization, use wasm-opt from Binaryen:
wasm-opt -Oz -o output_optimized.wasm pkg/rust_wasm_demo_bg.wasm
This typically saves an additional 10-20% on top of Rust’s release optimizations.
Best Practices and Gotchas
When to use WASM: Computationally intensive tasks like parsing, compression, encryption, image/video processing, or physics simulations. If you’re doing heavy number crunching in a loop, WASM will likely help.
When to avoid WASM: Simple DOM manipulation, event handlers, or functions called very frequently with small payloads. The FFI boundary has overhead—passing data between JavaScript and WASM isn’t free. If a function takes 10 microseconds but FFI overhead adds 5 microseconds, you’ve only gained 2x speedup, not the 10x you might expect.
Bundle size matters: A minimal Rust WASM module is ~20KB gzipped, but it grows with dependencies. Avoid pulling in large crates for trivial tasks. Use cargo tree to audit your dependency graph.
Debugging: Enable source maps in Cargo.toml:
[profile.release]
debug = true
Use browser DevTools to step through Rust code. Chrome and Firefox both support WASM debugging with source maps.
Memory management: When passing large data structures, consider keeping them in WASM memory and passing pointers instead of copying. Use js-sys and web-sys for working with JavaScript objects without serialization overhead.
Error handling: Return Result<T, JsValue> from exported functions to propagate errors to JavaScript properly. The ? operator works seamlessly across the FFI boundary.
Rust and WebAssembly together give you the performance of native code with the reach of the web. Start with a performance-critical module, measure the impact, and expand from there. The tooling is mature, the ecosystem is rich, and the performance gains are real.