Priyanshu Mahey.

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-cli

Project 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 source

1) Create the Rust crate

mkdir -p wasm && cd wasm && cargo init --lib

Update 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-unknown

This 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.wasm

This 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 dev

You 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.


GitflowA client-side git implementation built with Rust and WebAssembly