What Is WebAssembly and How Does Rust Support It?

Web
WebAssembly enables high-performance web execution, and Rust compiles to it via dedicated targets like wasm32-unknown-unknown.

When JavaScript hits the wall

You have a Rust library that processes images, crunches physics simulations, or parses complex files. It runs in milliseconds on your machine. Now you want to run that same logic inside a web browser. You try porting the algorithm to JavaScript, but the performance drops. The main thread freezes. The garbage collector pauses. The user experience suffers.

WebAssembly solves this. It lets you compile your Rust code to a binary format that browsers execute at near-native speed. You keep your Rust logic, your safety guarantees, and your performance. JavaScript handles the DOM and user interface. WebAssembly handles the heavy lifting.

What WebAssembly actually is

WebAssembly, or Wasm, is a binary instruction format for a stack-based virtual machine. It is not a programming language. It is a compilation target. You write code in Rust, C, C++, or Go, and compile it to Wasm. The browser contains a Wasm engine that decodes and executes the binary directly.

Think of Wasm as a universal adapter for code. Just as a power adapter lets you plug a device into any outlet, Wasm lets you run compiled code in any environment that supports it. Browsers have supported Wasm for years. Server runtimes like Wasmtime and Wasmer also execute Wasm modules.

Wasm has a strict memory model. It uses linear memory, which is a contiguous block of bytes that grows as needed. Rust maps perfectly to this model. Rust's heap allocations live inside Wasm's linear memory. The browser can access this memory through JavaScript, but Rust manages the layout and lifetime of the data.

Rust does not need a garbage collector in Wasm. The Wasm memory is just a buffer. Rust's ownership system tracks allocations and frees them when they go out of scope. This eliminates GC pauses and gives you predictable latency.

Rust and Wasm: A natural fit

Rust compiles to Wasm with zero configuration changes to your source code. You only change the target triple. The Rust compiler knows how to emit Wasm instructions. It handles the calling conventions, the memory layout, and the ABI.

Rust's zero-cost abstractions shine in Wasm. Traits, generics, and iterators compile down to efficient Wasm code. You do not pay for the abstractions. The resulting Wasm module is often smaller and faster than hand-written C.

Rust's safety guarantees travel with you. The borrow checker still runs. You cannot have null pointer dereferences, buffer overflows, or data races in your Wasm module. The Wasm sandbox provides isolation, and Rust provides memory safety. You get double protection.

Minimal example: Exposing a function

To compile Rust to Wasm, you need the wasm32-unknown-unknown target. Add it to your toolchain.

rustup target add wasm32-unknown-unknown

Create a library crate. The crate type must be cdylib so the compiler emits a dynamic library suitable for Wasm.

# Cargo.toml
[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

The wasm-bindgen crate generates the glue code that lets JavaScript call your Rust functions. It handles type conversions and memory management.

// src/lib.rs
use wasm_bindgen::prelude::*;

/// Add two integers and return the result.
/// The macro generates JavaScript bindings automatically.
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

The #[wasm_bindgen] attribute marks the function for export. The macro inspects the signature and creates the necessary FFI layer. You do not write unsafe code here. The macro generates safe wrappers.

Build the module in release mode. Release mode enables optimizations that drastically reduce code size and improve speed.

cargo build --target wasm32-unknown-unknown --release

The output is a .wasm file in target/wasm32-unknown-unknown/release/. This file contains the compiled bytecode. You can load it in a browser or a Wasm runtime.

Convention aside: The community standard is to use wasm-pack instead of raw cargo build. wasm-pack runs cargo build, generates the JavaScript glue code, and packages everything for npm. It saves you from manual bundling steps.

How the browser runs your code

When a browser loads a Wasm module, it fetches the .wasm file and instantiates it. Instantiation allocates the linear memory and initializes the exports. The browser compiles the Wasm bytecode to machine code using a JIT compiler. This happens quickly, often in parallel with other tasks.

JavaScript interacts with the Wasm module through imports and exports. Exports are functions or memory buffers that Rust exposes. Imports are functions or memory that JavaScript provides to Rust.

Rust's std library works in Wasm, but with limitations. The wasm32-unknown-unknown target assumes no operating system. There is no filesystem, no network stack, and no threads by default. You interact with the environment through JavaScript APIs.

If you need to print to the console, println! does not work out of the box. The Wasm module has no access to the console. You must use web-sys or console_error_panic_hook to bridge the gap.

Convention aside: Always install console_error_panic_hook in browser targets. Without it, Rust panics produce cryptic Wasm traps. The hook converts panics to readable JavaScript errors with stack traces.

// src/lib.rs
use wasm_bindgen::prelude::*;

/// Initialize panic handling for better debugging.
/// Call this once at the start of your module.
#[wasm_bindgen(start)]
pub fn main() {
    // Set the panic hook to print errors to the JS console.
    console_error_panic_hook::install();
}

The #[wasm_bindgen(start)] attribute ensures the function runs when the module instantiates. This is the standard place for initialization logic.

Realistic example: String processing

Passing strings between JavaScript and Rust requires care. JavaScript uses UTF-16. Rust uses UTF-8. Wasm has no native string type. You must encode strings as bytes.

wasm-bindgen handles this automatically. When you use &str or String in a #[wasm_bindgen] function, the macro generates code to convert between UTF-8 and UTF-16.

// src/lib.rs
use wasm_bindgen::prelude::*;

/// Reverse a string and return the result.
/// The macro handles UTF-8 to UTF-16 conversion.
#[wasm_bindgen]
pub fn reverse_string(input: &str) -> String {
    input.chars().rev().collect()
}

The function takes a &str and returns a String. The generated glue code copies the input string from JavaScript memory to Wasm memory, performs the reversal, allocates a new string in Wasm memory, and copies the result back to JavaScript.

This works seamlessly, but it involves memory copies. For large strings, consider passing a pointer and length to avoid allocations. wasm-bindgen supports this pattern, but it requires unsafe code. Stick to the safe API unless profiling proves the copies are the bottleneck.

Ah-ha: Wasm memory is shared. JavaScript can read and write Wasm memory directly using WebAssembly.Memory. You can pass large buffers by reference instead of copying. This is how high-performance Wasm modules handle images and audio data.

Pitfalls and gotchas

The wasm32-unknown-unknown target is bare. It does not include the WASI API. You cannot use std::fs, std::net, or std::env. Attempting to link these crates results in linker errors.

If you try to use std::fs in a browser target, the compiler rejects the code. The error mentions missing symbols for WASI functions.

error[E0277]: the trait bound `std::fs::File: wasm_bindgen::convert::IntoWasmAbi` is not satisfied

This error appears when you try to pass a type that wasm-bindgen does not support. std::fs::File is not a valid Wasm type. The macro cannot generate bindings for it.

Use wasm32-wasip1 when you need filesystem or network access. This target includes the WASI API. It works in Node.js, Wasmtime, and Cloudflare Workers. It does not work in browsers.

Another pitfall is cargo run. You cannot run Wasm modules with cargo run. The host machine does not know how to execute Wasm. You need a runner.

error: could not execute target `wasm32-unknown-unknown`

This error occurs when you try to run a Wasm binary directly. Use wasm-pack test --node to run in Node.js. Use wasm-pack test --headless --chrome to run in a browser. Or use wasmtime for server-side execution.

Convention aside: wasm-opt is the standard tool for shrinking Wasm binaries. Rust's release mode optimizes well, but wasm-opt applies Wasm-specific passes that reduce size further. Run wasm-opt -O2 on your output to shave off kilobytes.

Decision: Targets and tools

Use wasm32-unknown-unknown when you are targeting web browsers. This target assumes no operating system. You cannot access the filesystem or network directly. You interact with the environment through JavaScript APIs.

Use wasm32-wasi-preview1 when you are deploying to server-side runtimes like Node.js, Wasmtime, or Cloudflare Workers. This target includes the WASI API, giving you access to filesystems, environment variables, and clocks.

Use wasm-bindgen when you need to call JavaScript from Rust or pass complex types like strings and arrays. The macro generates the necessary glue code to bridge the type systems.

Use wasm-pack when you want to build, test, and publish your Wasm module as a JavaScript package. It handles the bundling and npm publishing workflow.

Reach for js-sys and web-sys when you need to access specific browser APIs that wasm-bindgen does not wrap automatically. These crates provide type-safe bindings to the Web Platform.

Reach for raw pointers and unsafe only when you are implementing a custom allocator or interfacing with low-level Wasm features. The safe abstractions cover 99% of use cases. Isolate unsafe in small helper functions.

Where to go next