100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
WebAssembly

Building a Simple Wasm App

A hands-on walkthrough of building, compiling, and running a small WebAssembly module in both the browser and a standalone WASI runtime.

PracticeBeginner10 min readJul 10, 2026
Analogies

Setting Up the Project

The fastest path to a working Wasm module is Rust plus wasm-pack, since the toolchain handles binding generation, bundler compatibility, and packaging into an npm-ready module automatically. After installing Rust via rustup and running rustup target add wasm32-unknown-unknown, wasm-pack new my-wasm-app scaffolds a crate with a Cargo.toml pre-configured for a cdylib crate type, which is required because Wasm modules are compiled as dynamic libraries rather than standalone executables when targeting the browser. The generated lib.rs includes a #[wasm_bindgen] attribute on exported functions, which is the macro that auto-generates the JavaScript-facing glue code and TypeScript type definitions during the build.

🏏

Cricket analogy: Scaffolding a project with wasm-pack new is like a team's pre-season setup at a place like the National Cricket Academy, where the kit, nets, and fitness baseline are all prepared before a single ball is bowled in a real match.

Writing and Exporting Functions

Inside lib.rs, a function decorated with #[wasm_bindgen] and a pub fn signature becomes callable from JavaScript with automatic type marshaling for primitives like numbers, strings, and booleans. Complex types such as structs need #[wasm_bindgen] on the struct itself, and methods become JavaScript class methods on the generated binding. For performance-sensitive code that processes large arrays, it's more efficient to pass a &[u8] slice backed by shared linear memory rather than serializing data through JSON, since crossing the JS-Wasm boundary with structured data has real marshaling cost that scales with payload size.

🏏

Cricket analogy: Exporting a function with #[wasm_bindgen] is like naming a designated non-striker's runner in an old rule set — a formal declaration that makes a specific role visible and usable to the rest of the match officials.

rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => {
            let (mut a, mut b) = (0u64, 1u64);
            for _ in 2..=n {
                let c = a + b;
                a = b;
                b = c;
            }
            b
        }
    }
}

#[wasm_bindgen]
pub fn sum_bytes(data: &[u8]) -> u32 {
    data.iter().map(|&b| b as u32).sum()
}

Building and Loading in the Browser

Running wasm-pack build --target web produces a pkg directory containing the .wasm binary, a JavaScript loader, and .d.ts type definitions ready to import directly with an ES module import statement. The loader's default export is an async init function that must be awaited before calling any exported Wasm function, because instantiation itself is asynchronous — the browser needs to fetch, compile, and instantiate the binary via WebAssembly.instantiateStreaming under the hood. Once initialized, calling fibonacci(50) from JavaScript executes inside the Wasm linear memory sandbox, and the wasm-bindgen glue automatically converts the u64 return value into a JavaScript BigInt since JS numbers can't represent the full 64-bit range precisely.

🏏

Cricket analogy: Awaiting the async init function before calling Wasm exports is like waiting for the pitch report and toss result before setting a batting order — you can't finalize your plan until that preliminary step completes.

javascript
import init, { fibonacci, sum_bytes } from './pkg/my_wasm_app.js';

async function run() {
  await init(); // must await before calling exports
  console.log(fibonacci(50)); // returns a BigInt

  const bytes = new Uint8Array([1, 2, 3, 4, 5]);
  console.log(sum_bytes(bytes)); // 15
}

run();

Use wasm-pack build --target bundler when integrating with webpack or Vite, --target web for direct ES module usage without a bundler, and --target nodejs when the module runs in a Node.js server context instead of a browser.

Passing large JavaScript objects across the boundary via serde-serialized JSON (using #[wasm_bindgen] with serde_wasm_bindgen) is convenient but has real overhead — for hot paths processing large arrays or buffers, use typed arrays backed by linear memory instead of JSON serialization.

  • wasm-pack new scaffolds a Rust crate configured with crate-type = ["cdylib"] for Wasm compilation.
  • #[wasm_bindgen] on functions and structs generates the JavaScript glue code and TypeScript definitions automatically.
  • Primitives marshal automatically across the boundary; large buffers should use linear-memory-backed typed arrays instead of JSON for performance.
  • wasm-pack build --target web/bundler/nodejs produces output tailored to the consuming environment.
  • The generated init() function is async and must be awaited before calling any exported Wasm function.
  • Rust's u64 return values become JavaScript BigInt since JS numbers can't represent the full 64-bit range.
  • Choose the JSON/serde path for convenience with small structured data, and raw byte slices for performance-critical large payloads.

Practice what you learned

Was this page helpful?

Topics covered

#WebAssembly#WebAssemblyStudyNotes#WebDevelopment#BuildingASimpleWasmApp#Building#Simple#Wasm#App#StudyNotes#SkillVeris