Rust and WebAssembly
WebAssembly (often shortened to Wasm) lets you run code written in languages like Rust inside a web browser, next to JavaScript. Think of it as a second engine in the browser: JavaScript still drives the page, but when you have heavy work — image filters, physics, parsing big files — you can hand it to fast, compiled Rust code.
On this page you’ll write a tiny Rust library, compile it to Wasm, and call it from a web page. Nothing here runs in the Rust Playground (it needs a browser and extra tools), so most examples are plain code blocks for your own machine. The pure-Rust parts, though, you can try right away.
What WebAssembly is, and why Rust fits
Section titled “What WebAssembly is, and why Rust fits”WebAssembly is a compact binary format for programs. Every major browser can run it, at close to native speed, inside the same security sandbox as JavaScript. It isn’t only for browsers: servers and edge platforms run Wasm too.
Rust is one of the most popular languages for Wasm, for a few reasons:
- No garbage collector or big runtime. Rust doesn’t need to ship extra machinery with your code, so
.wasmfiles can be small. - Great tools.
wasm-bindgenwrites the glue code between Rust and JavaScript for you, andwasm-packbuilds everything in one command. - Safety and speed. You get the same memory safety and performance you’d get on a desktop.
Set up the tools
Section titled “Set up the tools”First, tell Rust it can compile for the Wasm target. A target is “the kind of machine the code will run on”; wasm32-unknown-unknown means “32-bit Wasm, no particular operating system”:
rustup target add wasm32-unknown-unknownNext, install wasm-pack. It compiles your crate, runs wasm-bindgen, and puts everything you need into one folder. Either use the official installer, or build it with Cargo:
# Linux / macOS installer from the wasm-pack websitecurl https://wasm-bindgen.github.io/wasm-pack/installer/init.sh -sSf | sh
# or, on any system with Rust installedcargo install wasm-pack --lockedWrite a Rust library for Wasm
Section titled “Write a Rust library for Wasm”A Wasm module is a library, not a program with main. Create one:
cargo new --lib hello-wasmcd hello-wasmThen edit Cargo.toml. The crate-type line tells Cargo to produce a .wasm-friendly library (cdylib), and wasm-bindgen is the crate that connects Rust and JavaScript:
[package]name = "hello-wasm"version = "0.1.0"edition = "2021"
[lib]crate-type = ["cdylib", "rlib"]
[dependencies]wasm-bindgen = "0.2"Now write two functions in src/lib.rs. The #[wasm_bindgen] attribute marks each one as “callable from JavaScript”:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]pub fn add(a: i32, b: i32) -> i32 { a + b}
#[wasm_bindgen]pub fn greet(name: &str) -> String { format!("Hello, {name}! This string was built in Rust.")}That’s all the Rust you need. Notice they’re ordinary functions: wasm-bindgen takes care of turning a JavaScript string into a &str and the returned String back into a JavaScript string.
Test the logic natively first
Section titled “Test the logic natively first”Here’s a habit that saves a lot of time: keep the real work in plain Rust functions, and test them on your normal computer with cargo test or a quick main. Debugging in a browser is slower than debugging in a terminal. The same logic, without the attribute, runs anywhere:
fn add(a: i32, b: i32) -> i32 { a + b}
fn greet(name: &str) -> String { format!("Hello, {name}! This string was built in Rust.")}
fn main() { println!("{}", greet("Ferris")); println!("2 + 3 = {}", add(2, 3));}Output:
Hello, Ferris! This string was built in Rust.2 + 3 = 5Once the logic is right, the #[wasm_bindgen] wrapper is just a thin door to the browser. (Tests marked #[test] also run natively with cargo test — see Testing.)
Build with wasm-pack
Section titled “Build with wasm-pack”From the crate folder, run:
wasm-pack build --target web--target web means “make files a browser can load directly with a <script type="module"> tag”, with no bundler like webpack needed. When we ran it, the new pkg/ folder contained:
pkg/├── hello_wasm_bg.wasm the compiled Rust code├── hello_wasm.js JavaScript glue that loads it and wraps each function├── hello_wasm.d.ts TypeScript types for your functions├── hello_wasm_bg.wasm.d.ts└── package.jsonThe dash in hello-wasm became an underscore in the file names. By default wasm-pack build makes a release (optimized) build; add --dev for a faster, unoptimized one while you experiment.
Call it from JavaScript
Section titled “Call it from JavaScript”Create index.html next to Cargo.toml (so pkg/ sits beside it):
<!doctype html><html lang="en"><head> <meta charset="utf-8"> <title>Hello from Rust</title></head><body> <p id="message">Loading…</p>
<script type="module"> import init, { add, greet } from "./pkg/hello_wasm.js";
await init(); // download and start the .wasm file
const text = greet("Ferris"); const sum = add(2, 3); document.getElementById("message").textContent = `${text} 2 + 3 = ${sum}`; </script></body></html>The default export, init(), downloads the .wasm file and starts it. After that, add and greet are normal JavaScript functions.
Browsers won’t load modules from a file:// address, so you need a small local web server. If you have Python installed, this is the quickest:
python3 -m http.server 8080Open http://localhost:8080 in your browser. When we did, the page showed:
Hello, Ferris! This string was built in Rust. 2 + 3 = 5That sentence was built by Rust, running inside the browser. 🦀
What crosses the boundary cheaply (and what doesn’t)
Section titled “What crosses the boundary cheaply (and what doesn’t)”Rust and JavaScript don’t share memory in the same way. The Wasm module has its own block of memory, and values have to travel between the two sides. Some trips are free; others cost a copy:
| Type in Rust | In JavaScript | Cost |
|---|---|---|
i32, u32, f64, bool |
number, boolean |
Cheap: passed directly |
&str, String |
string |
A copy, plus converting UTF-16 to UTF-8 (or back) |
&[u8], Vec<u8> |
Uint8Array |
A copy into (or out of) Wasm memory |
&mut [u8] |
Uint8Array |
Copied in, then copied back after the call |
So the golden rule is: make few calls that each do a lot of work. Calling Rust once to process a whole image is fast. Calling Rust a million times, once per pixel, spends most of its time crossing the boundary.
Here’s what that looks like. This function brightens a whole buffer of pixel values in one call:
use wasm_bindgen::prelude::*;
/// Make every pixel brighter. JS passes a Uint8Array.#[wasm_bindgen]pub fn brighten(pixels: &mut [u8], amount: u8) { for p in pixels.iter_mut() { *p = p.saturating_add(amount); }}From JavaScript, after await init(), you’d call brighten(pixels, 20) with a Uint8Array, and the array is updated in place. The loop itself is pure Rust, so you can test it natively:
fn brighten(pixels: &mut [u8], amount: u8) { for p in pixels.iter_mut() { // saturating_add stops at 255 instead of overflowing *p = p.saturating_add(amount); }}
fn main() { let mut pixels = [10, 100, 250]; brighten(&mut pixels, 20); println!("{pixels:?}");}Output:
[30, 120, 255]Notice 250 + 20 became 255, not a crash or a wrap-around: saturating_add stops at the maximum. That’s exactly what you want for colors.
Keep the .wasm file small
Section titled “Keep the .wasm file small”Every byte of your .wasm file has to be downloaded before the page can use it. Some tips:
- Always ship the release build. For our tiny example,
wasm-pack build --devproduced a 58,591-byte.wasmfile; the default release build was 21,530 bytes. (That’s from our machine withwasm-optturned off — your numbers will vary.) - Let wasm-pack run
wasm-opt. In release mode, wasm-pack runs Binaryen’swasm-optoptimizer on the output by default, which usually shrinks it further. - Turn on compression on your server. Our 21,530-byte file was 8,464 bytes after gzip.
- Try size-focused settings, then measure:
[profile.release]opt-level = "z" # optimize for size ("s" is a milder version)lto = truecodegen-units = 1Be honest with yourself and measure. For our two-function example, these settings didn’t help — the file came out at 22,817 bytes, slightly bigger. They tend to pay off in larger crates. Things like format! and panic messages also pull in code, so a crate that avoids them can be much smaller.
Where Wasm is used in the real world
Section titled “Where Wasm is used in the real world”A few examples, each with the company’s own write-up:
- Figma compiles its C++ design editor to WebAssembly and reported that it cut load time by 3x. (Not Rust, but it shows what Wasm can do in a big app.)
- Cloudflare Workers supports Rust through the
workers-rscrate: your Rust code is compiled to Wasm and runs on Cloudflare’s servers around the world. - Shopify Functions are WebAssembly modules that customize stores, and Shopify recommends Rust as the most performant language for them.
- 1Password’s browser extension moved its page filling to WebAssembly and reported it running at least twice as fast.
Common mistakes
Section titled “Common mistakes”Forgetting crate-type = ["cdylib"]. Without it, wasm-pack stops right away. Happily, the error tells you exactly what to add:
Error: crate-type must be cdylib to compile to WebAssembly. Add the following to your Cargo.toml file:
[lib]crate-type = ["cdylib", "rlib"]Opening index.html by double-clicking it. The page stays on “Loading…”, and the browser console (in Chrome) says:
Access to script at 'file:///…/pkg/hello_wasm.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: chrome, chrome-extension, chrome-untrusted, data, http, https, isolated-app.The fix: serve the folder with a local web server, like python3 -m http.server, and open the http://localhost address instead.
Calling a function before await init(). The Wasm module isn’t loaded yet, so the glue code has nothing to call. When we removed the init() line, Chrome showed Cannot read properties of undefined (reading '__wbindgen_free'). Always await init() first.
Next steps: full web apps in Rust
Section titled “Next steps: full web apps in Rust”Calling a few Rust functions from JavaScript is a great start. If you want to build the whole web app in Rust, look at these frameworks. Both compile to Wasm and let you write components in Rust, a bit like React:
- Leptos — a fast, modern framework that can also render pages on the server.
- Yew — a mature, component-based framework.
- WebAssembly is a fast, safe binary format that browsers (and servers) can run; Rust compiles to it well because it has no garbage collector.
- Add the target with
rustup target add wasm32-unknown-unknown, and installwasm-pack. - Mark functions with
#[wasm_bindgen], setcrate-type = ["cdylib", "rlib"], and build withwasm-pack build --target web. - In JavaScript,
await init()first, then call your functions; serve the page overhttp://, notfile://. - Numbers cross the boundary cheaply; strings and slices are copied — so make few, chunky calls.
- Ship release builds with compression, and measure before trusting size tweaks.
Quick check
Section titled “Quick check”Quick check
1. Which command adds the Rust target for browser WebAssembly?
2. What does the #[wasm_bindgen] attribute do?
3. Your page stays on "Loading…" and the console shows a CORS error for a file:/// address. What's the fix?
4. Which of these is cheapest to pass from JavaScript to Rust?
Score: 0 / 4
Read more
Section titled “Read more”- Compiling from Rust to WebAssembly — MDN — a step-by-step tutorial that builds and runs a wasm-pack project.
- The wasm-bindgen Guide — every supported type, how to call browser APIs, and many examples.
- wasm-pack documentation — all the
buildoptions, targets, and profiles. - wasm32-unknown-unknown — The rustc book — what this target supports and its limits.
- WebAssembly on MDN — what Wasm is and how browsers run it, independent of Rust.