Choosing a Rust Wasm Target Triple

This page answers one question: Rust offers several WebAssembly targets — which one should a given project build for, and what changes in the code and the output when you pick each?

Prerequisites

  • [ ] Rust 1.81 or newer (wasm32-wasip2 and wasm32v1-none are tier 2 targets from 1.82 onward).
  • [ ] A clear idea of where the module will run: a browser, a WASI runtime, an edge platform, or an embedded host.
  • [ ] rustup target list | grep wasm to see what is installed.

A target triple describes the host, not the instruction set

All Rust WebAssembly targets produce the same kind of bytecode. What differs is what the compiler and the standard library assume about the environment that will run it — which functions it can import, how it gets memory, whether there is a file system, a clock, environment variables, or a way to print. That is what the triple encodes: an architecture (wasm32), a vendor, and an operating-system-like “environment” component.

wasm32-unknown-unknown assumes nothing. There is no operating system, so the standard library’s I/O, time, threading and process functions compile but fail at runtime — they return errors or panic. Everything the module needs from outside must be imported explicitly, which in browser projects means wasm-bindgen and web-sys.

wasm32-wasip1 assumes a host that implements WASI preview 1: a POSIX-flavoured set of imports for files, clocks, random numbers, environment and arguments. std::fs, std::env and println! work, because the standard library routes them to those imports. wasm32-wasip2 assumes a WASI preview 2 host and produces a component rather than a core module, using the component model’s typed interfaces.

wasm32v1-none assumes even less than unknown-unknown: no standard library at all (no_std only), and only WebAssembly 1.0 features, for hosts that cannot handle newer proposals. And wasm32-unknown-emscripten assumes an Emscripten-generated JavaScript runtime, which emulates a POSIX environment in the browser.

Rust Wasm targets and what each assumes Five Rust WebAssembly targets compared by what host they expect, whether std I/O works, what kind of binary they produce, and their typical use. target expected host std I/O output typical use wasm32-unknown-unknown none; imports by hand stubs core module browser via wasm-bindgen wasm32-wasip1 WASI preview 1 files, env, stdout core module CLI tools, edge, Node wasm32-wasip2 WASI preview 2 via WASI 0.2 component wasi:http, composition wasm32v1-none minimal, MVP only no_std only core module conservative embedders wasm32-unknown-emscripten Emscripten JS runtime emulated POSIX module + JS linking with C code

Step 1 — install and build for the candidates

Adding targets is cheap, and building the same crate for two of them is the fastest way to see the difference:

rustup target add wasm32-unknown-unknown wasm32-wasip1 wasm32-wasip2
cargo build --release --target wasm32-unknown-unknown
cargo build --release --target wasm32-wasip1
wasm-objdump -x -j Import target/wasm32-wasip1/release/app.wasm | head
Import[4]:
 - func[0] sig=2  <- wasi_snapshot_preview1.fd_write
 - func[1] sig=4  <- wasi_snapshot_preview1.environ_get
 - func[2] sig=4  <- wasi_snapshot_preview1.environ_sizes_get
 - func[3] sig=5  <- wasi_snapshot_preview1.proc_exit

The same crate built for wasm32-unknown-unknown has no such imports — or only the wasm-bindgen placeholders. That import list is the contract with the host: a module can only run where every import it declares is provided.

Step 2 — decide by where the module runs

For a browser module called from JavaScript, use wasm32-unknown-unknown with wasm-bindgen. It produces the smallest output, has the most mature tooling for the JavaScript boundary, and is what wasm-pack, Trunk and every Rust UI framework target. The missing standard I/O rarely matters, because in a browser you want web-sys calls anyway.

For a command-line tool, a server-side function on an edge platform, or a module run by Node’s node:wasi, use wasm32-wasip1. Ordinary Rust code — reading files, printing, parsing arguments — works unchanged. Runtimes from wasmtime to Cloudflare Workers support it.

For new server-side work that benefits from typed interfaces — an HTTP handler on wasi:http, a plugin defined by a WIT world, components composed together — use wasm32-wasip2. It is the direction WASI is moving, and its differences from preview 1 are covered in WASI preview 1 vs preview 2.

For a host that only implements WebAssembly 1.0 — some blockchain VMs, older embedded interpreters — use wasm32v1-none with #![no_std]. And use the Emscripten target only when you must link Rust with C or C++ code that was itself built for Emscripten; for most mixed projects, linking C and Rust objects into one module on wasm32-unknown-unknown is simpler.

Picking a Rust target from where the module runs A decision tree from the deployment environment to a target triple. Browser modules called from JavaScript use wasm32-unknown-unknown; WASI hosts use wasip1, or wasip2 for components; minimal MVP-only hosts use wasm32v1-none. Where will the module run? browser, via JavaScript wasm32-unknown-unknown with wasm-bindgen CLI, edge, Node WASI wasm32-wasip1 std I/O works components, wasi:http wasm32-wasip2 typed WIT interfaces MVP-only embedder wasm32v1-none no_std, 1.0 features

Step 3 — write code that compiles for more than one

Libraries often need to support several targets. Gate target-specific code on target_os and target_arch rather than on the triple string:

#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
fn now_ms() -> f64 {
    js_sys::Date::now()                  // browser: ask JavaScript
}

#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
fn now_ms() -> f64 {
    use std::time::{SystemTime, UNIX_EPOCH};
    SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs_f64() * 1000.0
}

On wasm32-unknown-unknown, target_os is "unknown"; on the WASI targets it is "wasi", with target_env distinguishing p1 from p2. Put browser-only dependencies such as js-sys behind a target-specific table in Cargo.toml so WASI builds do not pull them in:

[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
js-sys = "0.3"
wasm-bindgen = "=0.2.93"

Step 4 — check which proposals the target enables

Targets also differ in which post-MVP WebAssembly features the compiler uses by default. Recent Rust releases enable bulk-memory, mutable-globals, sign-ext, nontrapping-fptoint, multivalue and reference-types for the wasm32 targets, following LLVM’s defaults. Every current browser and major runtime supports them, but an older or minimal host may not — which is the reason wasm32v1-none exists.

rustc --print target-features --target wasm32-unknown-unknown | grep -E '^\s+(bulk|simd|reference|multivalue)'
rustc --print cfg --target wasm32-unknown-unknown -C target-cpu=generic | grep target_feature

Features can be turned on or off per build with -C target-feature, for example +simd128 to allow SIMD instructions. Detecting what a host supports before loading the module is covered in detecting proposal support at runtime.

Step 5 — know what fails silently on unknown-unknown

The biggest trap of wasm32-unknown-unknown is that code compiles fine and then misbehaves at runtime. The standard library’s platform functions exist as stubs:

let t = std::time::Instant::now();          // panics: "time not implemented on this platform"
let f = std::fs::read("config.toml");       // returns Err(Unsupported)
println!("hello");                          // goes nowhere — no stdout
std::thread::spawn(|| {});                  // returns an error / panics

None of these is a compile error, which is why a crate that “supports wasm32” may still not work in a browser. The remedies — browser-backed alternatives, feature flags, and finding which dependency calls what — are covered in fixing crates that fail to compile for Wasm and logging from Rust Wasm to the browser console.

Changing targets later

The target is not a one-way door, and it is worth knowing what moving between them costs. Moving a browser crate from wasm32-unknown-unknown to a WASI target means replacing every wasm-bindgen import with a WASI or component interface — the computational core moves unchanged, the boundary layer is rewritten. Moving a CLI tool from wasip1 to wasip2 is mostly a rebuild, because the standard library hides the difference, plus updating how the host runs it. Moving anything to wasm32v1-none means giving up std, which is a real port.

That is a good argument for keeping the boundary layer thin and separate from day one. A crate structured as a pure core library plus a small target-specific adapter — a #[wasm_bindgen] module for the browser, a main.rs for WASI — can add a second target in an afternoon. A crate with web_sys calls spread through its business logic cannot.

Expected output

The same “hello world” crate built for each target shows the different environments clearly:

target                         size     imports
wasm32-unknown-unknown         14 KB    (none)
wasm32-wasip1                  71 KB    wasi_snapshot_preview1.* (4)
wasm32-wasip2                  92 KB    wasi:cli/*, wasi:io/* (component)
wasm32v1-none (no_std)          0.4 KB  (none)

Running the WASI build prints to the terminal; the unknown-unknown build prints nothing at all, because println! has nowhere to go.

Gotchas

  • error[E0463]: can't find crate for std on wasm32v1-none. That target has no standard library. Add #![no_std] and use core and alloc with your own allocator.
  • A WASI module fails in the browser with unknown imports. Browsers do not implement WASI. Use a shim as in running WASI modules in the browser, or build for unknown-unknown.
  • The old wasm32-wasi name. It was renamed to wasm32-wasip1 and removed in Rust 1.84. Update scripts and CI.
  • A wasip2 component will not instantiate with WebAssembly.instantiate. Components are not core modules. Transpile with jco for JavaScript hosts, or run in a component-aware runtime.

Performance note

Target choice affects size far more than speed. The WASI hello world is five times larger than the unknown-unknown one because println! pulls in formatting and the WASI I/O layer; the no_std build is tiny because it contains almost nothing. Compute-heavy code runs at the same speed on every target — the instructions are the same.

Hello-world module size by target Release builds of the same minimal crate for four Rust WebAssembly targets, after wasm-opt -Oz. The difference is almost entirely the standard library's I/O and formatting support. size in KB after wasm-opt -Oz wasm32v1-none, no_std 0.4 KB wasm32-unknown-unknown 14 KB wasm32-wasip1 71 KB wasm32-wasip2 component 92 KB

Frequently Asked Questions

Can one crate target the browser and WASI from the same source? Yes, with cfg gates for the platform-specific parts. Many libraries do exactly that, and the computational core is usually target-independent anyway.

Is wasm64 available? wasm64-unknown-unknown exists as a tier 3 target for the memory64 proposal. It is useful for very large heaps and not yet a default choice; see Memory64 and large heaps.

Which target does Cloudflare Workers use? Rust Workers use wasm32-unknown-unknown with wasm-bindgen through the worker crate; Workers’ WASI support covers other cases. See deploying Wasm to Cloudflare Workers.

Does the target affect threads? Threads need the atomics and bulk-memory features and a rebuilt standard library on unknown-unknown; the wasm32-wasip1-threads target provides them for WASI hosts that support the threads proposal.

← Back to Rust to Wasm Compilation Guide