Building with Web Assembly
How to actually integrate WASM into your web dev
I've been curious about WASM for a while, but my question has always been: how do I actually integrate it into my web app?
In this post, I build a small Rust function, compile it to WASM, and call it from a React app. That's it — a minimal end-to-end example.
Stack: TanStack Start, Vite, Bun, Rust + wasm-bindgen
Quick WASM Primer
WebAssembly is a compilation target. Languages like Rust compile to .wasm, and JavaScript loads that module and calls its functions. It's not trying to replace JS — it lets you run performance-critical code in a sandboxed environment.
The runtime flow is simple: fetch the .wasm bytes → compile → instantiate → call exports from JavaScript.
One catch: strings and complex data don't cross the JS↔WASM boundary easily. That's why wasm-bindgen exists — it generates the glue code so Rust functions feel natural to call from JavaScript.
When does WASM actually make sense?
WASM shines for CPU-heavy work: parsing, compression, image processing, crypto, simulations. It's also useful when you want to share code across web and desktop (via Tauri) or when Rust's memory safety matters.
It's not worth it for simple CRUD or UI logic. The overhead of crossing the JS↔WASM boundary means you should save it for places where it clearly pays off.
Under the hood
You don't need to write raw loading code — wasm-bindgen handles it. But if you're curious, check MDN's guide. The browser fetches the .wasm bytes, compiles, instantiates, and then you call exports.
The goal
A Rust function that returns "Hello from Rust, TanStack!", callable from React. Let's build it.
Prerequisites
You'll need Bun and Rust installed. Then add the WASM target:
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cliProject layout
Rust lives in wasm/, and the generated artifacts go to src/wasm/ so React can import them.
├─ src/wasm/ # Generated JS glue + .wasm
├─ wasm/ # Rust source1) Create the Rust crate
mkdir -p wasm && cd wasm && cargo init --libUpdate wasm/Cargo.toml:
[package]
name = "hello_wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"(cdylib tells Rust to produce a dynamic library — what the WASM toolchain expects.)
2) Write the Rust function
wasm/src/lib.rs:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn hello(name: &str) -> String {
format!("Hello from Rust, {}!", name)
}#[wasm_bindgen] makes this callable from JavaScript.
3) Compile to WASM
cd wasm && cargo build --target wasm32-unknown-unknownThis produces wasm/target/wasm32-unknown-unknown/debug/hello_wasm.wasm.
4) Generate JS bindings
Raw .wasm isn't ergonomic to import. wasm-bindgen generates the glue:
wasm-bindgen \
--target web \
--out-dir ../src/wasm \
--no-typescript \
target/wasm32-unknown-unknown/debug/hello_wasm.wasmThis creates src/wasm/hello_wasm.js and hello_wasm_bg.wasm.
5) Add a loader wrapper
The generated module needs async initialization. Create src/wasm/hello.ts:
import init, { hello } from "./hello_wasm";
let initialized = false;
export async function helloFromWasm(name: string) {
if (!initialized) {
await init();
initialized = true;
}
return hello(name);
}6) Call it from React
src/routes/index.tsx:
import { useEffect, useState } from "react";
import { helloFromWasm } from "../wasm/hello";
export default function Home() {
const [msg, setMsg] = useState("Loading...");
useEffect(() => {
helloFromWasm("TanStack").then(setMsg);
}, []);
return <p>{msg}</p>;
}7) Run it
bun install && bun devYou should see: Hello from Rust, TanStack!
Whenever you change Rust code, just rebuild with cargo build --target wasm32-unknown-unknown and rerun the wasm-bindgen command.