WebAssembly

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Rust 1.99.0, wasm-bindgen 0.2.129 and the 2024 edition, checked on 2026-10-06. WebAssembly (Wasm) is a portable, sandboxed binary instruction format that browsers, server-side runtimes and plugin hosts can execute at near-native speed. Rust is one of the best-supported languages for it: no garbage collector or runtime to ship, small binaries, and first-class targets in the compiler. The examples below were built for each target and run in Node.js.

flowchart LR crate["Rust crate"] --> t1["cargo build --target wasm32-unknown-unknown"] crate --> t2["cargo build --target wasm32-wasip1 / wasm32-wasip2"] t1 --> wasm1["module.wasm
(no system interface)"] wasm1 --> bg["wasm-bindgen CLI
generates JS glue + .d.ts"] bg --> browser["Browser / bundler
(JS calls Rust, Rust calls DOM via web-sys)"] bg --> node["Node.js"] t2 --> wasm2["WASI module or component"] wasm2 --> host["WASI host: wasmtime, Node WASI,
edge platforms, plugin systems"]

Targets

Target Use

wasm32-unknown-unknown

no operating-system interface at all; the host supplies every import. Used for browsers (with wasm-bindgen) and for hosts with a custom API

wasm32-wasip1

WASI preview 1: a POSIX-like system interface (files, clock, random, args, stdio). Formerly named wasm32-wasi: renamed in Rust 1.78, and the old name was removed in 1.84

wasm32-wasip2

WASI preview 2 (0.2), built on the component model: produces a component with typed imports and exports described in WIT

wasm32v1-none

a minimal target restricted to the WebAssembly 1.0 feature set, for embedded and very old hosts

rustup target add wasm32-unknown-unknown wasm32-wasip1 wasm32-wasip2
cargo build --release --target wasm32-wasip1    # target/wasm32-wasip1/release/shelf-wasi.wasm

Ordinary Rust compiles to these targets, with the obvious limits: no threads by default, no direct sockets in the browser, and file access only through what the host provides (WASI). Crates that need the OS — tokio’s networking, most of std::fs in the browser — do not work on wasm32-unknown-unknown.

WASI: command-line programs and the component model

A WASI program is a normal Rust binary. This one sorts and de-duplicates titles read from standard input:

use std::io::{self, BufRead, Write};

/// A WASI command: reads titles from stdin, writes the sorted, de-duplicated list to stdout.
fn main() -> io::Result<()> {
    let mut titles: Vec<String> = io::stdin().lock().lines().collect::<Result<_, _>>()?;
    titles.sort();
    titles.dedup();
    let mut out = io::stdout().lock();
    for t in &titles {
        writeln!(out, "{t}")?;
    }
    eprintln!("{} unique titles (compiled for {})", titles.len(), std::env::consts::ARCH);
    Ok(())
}

Run it in any WASI host. With Node.js’s built-in WASI support:

import { readFile } from 'node:fs/promises';
import { WASI } from 'node:wasi';
const wasi = new WASI({ version: 'preview1', args: ['shelf-wasi'] });
const module = await WebAssembly.compile(await readFile('target/wasm32-wasip1/release/shelf-wasi.wasm'));
const instance = await WebAssembly.instantiate(module, wasi.getImportObject());
wasi.start(instance);
cargo build --release -p shelf-wasi --target wasm32-wasip1
printf 'Emma\nDune\nEmma\n' | node --no-warnings run-wasi.mjs
# Dune
# Emma
# 2 unique titles (compiled for wasm32)

With wasmtime, the Bytecode Alliance’s reference runtime: wasmtime run target/wasm32-wasip1/release/shelf-wasi.wasm. Hosts grant capabilities explicitly — a WASI program sees only the directories and environment the host passes in (wasmtime run --dir data::/data ...), which is what makes Wasm attractive for plugins and multi-tenant edge platforms.

The component model (WASI 0.2, target wasm32-wasip2) goes further: components declare typed interfaces in WIT (WebAssembly Interface Types), so a component written in Rust can be composed with one written in another language and called with strings, records and lists rather than raw integers. wit-bindgen generates Rust bindings from WIT files, and cargo component builds components from a WIT world.

wasm-bindgen: Rust in the browser

wasm32-unknown-unknown modules can only exchange numbers with JavaScript. wasm-bindgen adds the rest: strings, objects, classes, closures, promises and exceptions, by generating JavaScript glue code and TypeScript definitions from #[wasm_bindgen] annotations. js-sys binds JavaScript built-ins (Array, Promise, Date) and web-sys binds every Web API (DOM, fetch, Canvas, WebGL), each API behind a Cargo feature.

[package]
name = "shelf-wasm"
version = "0.1.0"
edition = "2024"

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

[dependencies]
wasm-bindgen = "0.2.129"
web-sys = { version = "0.3", features = ["console", "Document", "Element", "HtmlElement", "Window"] }
js-sys = "0.3"
use wasm_bindgen::prelude::*;

/// Exported to JavaScript as `isbnIsValid(text)`.
#[wasm_bindgen(js_name = isbnIsValid)]
pub fn isbn_is_valid(text: &str) -> bool {
    let digits: Vec<u32> = text.chars().filter_map(|c| c.to_digit(10)).collect();
    digits.len() == 13
        && digits.iter().enumerate().map(|(i, d)| d * if i % 2 == 0 { 1 } else { 3 }).sum::<u32>().is_multiple_of(10)
}

/// A Rust struct exposed as a JavaScript class.
#[wasm_bindgen]
pub struct Shelf {
    titles: Vec<String>,
}

#[wasm_bindgen]
impl Shelf {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Shelf {
        Shelf { titles: Vec::new() }
    }

    pub fn add(&mut self, title: String) {
        self.titles.push(title);
    }

    /// Returns a JavaScript array of matching titles.
    pub fn search(&self, query: &str) -> js_sys::Array {
        let q = query.to_lowercase();
        self.titles.iter().filter(|t| t.to_lowercase().contains(&q)).map(|t| JsValue::from_str(t)).collect()
    }

    #[wasm_bindgen(getter)]
    pub fn size(&self) -> usize {
        self.titles.len()
    }
}

impl Default for Shelf {
    fn default() -> Self {
        Self::new()
    }
}

// Import a JavaScript function into Rust.
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(message: &str);
}

/// Runs when the module is instantiated: writes into the page through web-sys.
#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {
    log("shelf-wasm loaded");
    let Some(document) = web_sys::window().and_then(|w| w.document()) else {
        return Ok(()); // not in a browser (for example under Node.js)
    };
    if let Some(status) = document.get_element_by_id("status") {
        status.set_text_content(Some("Catalogue ready"));
    }
    Ok(())
}

Generate the glue with the wasm-bindgen CLI, whose version must match the wasm-bindgen crate exactly:

cargo install wasm-bindgen-cli --version 0.2.129
cargo build --release --target wasm32-unknown-unknown -p shelf-wasm
wasm-bindgen --target web --out-dir pkg target/wasm32-unknown-unknown/release/shelf_wasm.wasm
ls pkg
# shelf_wasm.d.ts  shelf_wasm.js  shelf_wasm_bg.wasm  shelf_wasm_bg.wasm.d.ts
<!doctype html>
<p id="status">Loading...</p>
<script type="module">
  import init, { isbnIsValid, Shelf } from "./pkg/shelf_wasm.js";
  await init();                                   // fetches and instantiates shelf_wasm_bg.wasm, runs #[wasm_bindgen(start)]
  console.log(isbnIsValid("978-0-441-01359-3"));  // true
  const shelf = new Shelf();
  shelf.add("Dune");
  shelf.add("Dune Messiah");
  console.log(shelf.search("messiah"), shelf.size); // ["Dune Messiah"] 2
</script>

The same build with --target nodejs produces a CommonJS module for Node.js, and --target bundler one for Vite or webpack. Keep the boundary coarse: every call that passes strings copies them between JavaScript’s UTF-16 and Rust’s UTF-8 in Wasm memory.

Packaging after the rustwasm sunset

The rustwasm GitHub organisation, home of wasm-pack, wasm-bindgen and the Rust and WebAssembly book, was sunset in 2025. wasm-bindgen moved to its own organisation and remains actively maintained (with its guide now at wasm-bindgen.github.io); wasm-pack and the Rust and WebAssembly book are archived and should be treated as legacy. Current options for packaging browser code:

  • the wasm-bindgen CLI directly, as above, plus wasm-opt (from Binaryen) to shrink the binary;

  • trunk, a build tool and dev server for Rust web applications;

  • the build tooling of a Rust front-end framework (cargo leptos, dx for Dioxus);

  • bundler plugins such as vite-plugin-wasm.

Linear memory

A Wasm module has a linear memory: one contiguous, growable byte array that holds the Rust heap and stack. JavaScript can see it as an ArrayBuffer, which is how wasm-bindgen copies strings in and out, and how large data (images, audio) can be shared without copying by passing a pointer and a length. Memory grows in 64 KiB pages and never shrinks; the default allocator is Rust’s dlmalloc port. Pointers are 32-bit (usize is 4 bytes) on wasm32; wasm64 targets with 64-bit memories exist but are not widely supported by hosts yet.

Size and performance

Release builds with opt-level = "s" or "z", lto = true and codegen-units = 1, followed by wasm-opt -Oz, are the usual recipe for small binaries. Avoid pulling in std::fmt-heavy code paths and large dependencies; twiggy shows what takes space. For CPU-bound work, Wasm usually runs within 1.5-2x of native speed; SIMD (target-feature=+simd128) and threads (with SharedArrayBuffer, which requires cross-origin isolation in browsers) can narrow the gap.

Rust front-end frameworks

Frameworks such as Leptos, Dioxus and Yew let you write whole web front ends in Rust, compiled to Wasm, with server-side rendering and hydration. Leptos and Dioxus integrate with axum for full-stack applications; they are covered on Axum: Full-stack Rust with Leptos & Dioxus. Mara & Holmes (Refactoring to Rust, chapter 10) use Yew 0.19; current Yew (0.21+) and the newer frameworks have different APIs.

WebAssembly in other stacks

Other ecosystems on this site target WebAssembly too: Kotlin/Wasm in Kotlin Multiplatform (Web with Kotlin/Wasm & JS), .NET in the browser with Blazor (Blazor WebAssembly, Hybrid & Deployment), and machine-learning inference with Transformers.js and ONNX Runtime Web (Hugging Face: Web & Cross-Platform). Rust’s niche among them is small, GC-free modules that can be dropped into an existing JavaScript application.

Where the books are outdated

Mara & Holmes (Refactoring to Rust, chapters 9-10) target wasm32-wasi — renamed wasm32-wasip1 in Rust 1.78 and removed in 1.84 — and use WasmEdge-specific forks (tokio_wasi, reqwest_wasi), wasm-bindgen 0.2.87 with wasm-pack, and Yew 0.19. Today: use wasm32-wasip1 or wasm32-wasip2 (the component model), mainline crates where they support WASI, the wasm-bindgen CLI or a framework’s tooling instead of the archived wasm-pack, and current framework versions.