Rust to WebAssembly: A Practical Guide to Toolchains and Workflows
Compiling Rust to WebAssembly unlocks CPU-heavy code for the web; here's how to navigate the toolchain from target selection to wasm-pack and trunk.

Rust's type safety and zero-cost abstractions have always made it a natural fit for performance-sensitive work. What's changed in recent years is how straightforward the path from `cargo build` to a browser-ready binary has become. Whether you're porting a physics engine, a codec, or cryptographic primitives to run on the web or an edge runtime, the Rust-to-WebAssembly pipeline is mature, well-documented, and genuinely usable in production.
This guide walks through the practical toolchain: choosing the right WASM compilation target, wiring up JavaScript interop with wasm-bindgen, and deciding between wasm-pack and trunk depending on what you're actually building.
Choosing your WASM target
The first decision in any Rust-to-WASM project is target selection, and it's one that shapes everything downstream. Rust's toolchain supports multiple WebAssembly targets, and the distinction matters.
`wasm32-unknown-unknown` is the workhorse for browser and edge deployments. It produces bare WASM with no assumed host environment, which makes it maximally portable. You're not pulling in WASI or any system interface layer, so the binary stays lean and loads fast. Add the target with:
rustup target add wasm32-unknown-unknown
`wasm32-wasi` (and its successor `wasm32-wasip1` in newer toolchain versions) is the better choice when you're deploying to a server-side WASM runtime like Wasmtime or WasmEdge, or anywhere that exposes a WASI interface. WASI gives your module access to file I/O, clocks, and standard streams in a sandboxed way. For pure browser work, you don't need it and the overhead isn't worth it.
`wasm32-wasi` also becomes relevant for edge compute platforms that have adopted WASI as their component interface. If you're targeting Fermyon Spin, Fastly Compute, or similar runtimes, check their documentation on which target they expect.
For CPU-heavy workloads you want to run in a browser worker or a Cloudflare Worker, `wasm32-unknown-unknown` paired with the rest of the toolchain described below is the right starting point.
wasm-bindgen: the bridge between Rust and JavaScript
Compiling to `wasm32-unknown-unknown` gets you a `.wasm` binary, but that binary can only natively pass numbers across the JS/WASM boundary. Strings, arrays, closures, DOM references, and Rust structs all require a translation layer. That's what wasm-bindgen provides.
wasm-bindgen is both a Rust crate and a CLI tool. You annotate your Rust functions and types with `#[wasm_bindgen]`, and the toolchain generates the JavaScript glue code that handles memory management and type conversion automatically. A function like:
#[wasm_bindgen] pub fn greet(name: &str) -> String { format!("Hello, {}!", name) }
becomes callable from JavaScript as though it were a native JS function, with the string encoding and memory allocation handled transparently behind the scenes.
wasm-bindgen also ships `web-sys` and `js-sys`, two companion crates that provide Rust bindings to the entire Web API surface and to JavaScript's built-in types respectively. Want to call `requestAnimationFrame` from Rust? That's `web_sys::window().unwrap().request_animation_frame(...)`. The coverage is comprehensive because the bindings are auto-generated from the WebIDL specs.
For interop-heavy projects, `gloo` is worth knowing about: it's a higher-level toolkit built on top of `web-sys` that wraps common browser APIs in more ergonomic Rust interfaces. It won't come up in every project, but it saves real time when you need it.

wasm-pack: the right tool for library authors
If you're building a Rust library that will be consumed by a JavaScript or TypeScript project, wasm-pack is the tool that turns a `#[wasm_bindgen]`-annotated crate into a publishable npm package.
Running `wasm-pack build` does several things in one step: it compiles your crate with the right target and flags, runs wasm-bindgen to generate the JS glue, optimizes the WASM binary with `wasm-opt` if it's available, and packages everything into a directory that's ready to publish to npm or consume via a bundler. The ` target` flag controls the output format:
- ` target bundler` produces output for webpack or Rollup (the default for most web projects)
- ` target web` generates ES modules you can load directly in a browser without a bundler
- ` target nodejs` wraps the module for use in Node.js
- ` target deno` targets Deno's import system
wasm-pack also handles the `pkg/` directory layout, the `package.json` generation, and TypeScript declaration files if your project uses them. For teams shipping WASM as a dependency, this automated packaging is the main reason wasm-pack exists.
Testing is built in too. `wasm-pack test headless firefox` runs your Rust tests in a real browser environment using wasm-bindgen-test, which is far more representative than running them under Node.js if your code touches browser APIs.
trunk: full-stack development for Rust-first web apps
Where wasm-pack targets the library-publishing use case, trunk is aimed at developers building entire web applications in Rust. It's the build tool and dev server you reach for when Rust is the application layer, not just a performance module you're calling from JS.
trunk works around a simple convention: it treats your `index.html` as the root of your project and handles asset compilation, WASM building, and hot reloading from there. You annotate your HTML with `data-trunk` attributes to tell it how to handle different asset types, and `trunk serve` starts a dev server with live reload. No webpack config, no JavaScript build system to maintain.
The canonical use case for trunk is building applications with frameworks like Leptos, Yew, or Dioxus, all of which are Rust-native front-end frameworks that compile to WASM and render to the DOM (or a virtual DOM) without any JavaScript application code. If you're writing a Yew component tree, trunk is the build tool the ecosystem assumes you're using.
trunk also handles CSS, Sass, static assets, and inline scripts alongside your WASM binary, so you're not stitching together multiple tools to get a working development environment. For production builds, `trunk build release` produces an optimized output directory ready to drop onto any static host.
Putting the pieces together
The practical workflow depends on what you're building. For a Rust library shipping to npm, the stack is: `wasm32-unknown-unknown` target, `wasm-bindgen` for interop annotations, and `wasm-pack build` to produce the distributable package. For a Rust-first web application, the stack is: the same target and `wasm-bindgen` underpinning a framework like Leptos or Yew, with `trunk serve` handling the development loop.
In both cases, WASM binary size is worth monitoring. Release builds with `opt-level = "s"` or `"z"` in your `Cargo.toml` profile, combined with `wasm-opt` (which wasm-pack runs automatically), typically produce binaries small enough for fast initial loads. The `twiggy` tool is useful for auditing what's contributing to binary size if you need to trim further.
The Rust-to-WebAssembly toolchain has matured to the point where the friction is low enough that the decision to use it really does come down to whether WASM is the right architectural choice for your workload. If the answer is yes, these tools get you there without fighting the environment the entire way.
Know something we missed? Have a correction or additional information?
Submit a Tip

