Skip to content
Volume W · The TensorCore.si Library

WebAssembly

Near-native speed in every browser.

WebAssembly (Wasm) is a compact binary instruction format that runs in all modern browsers and in standalone runtimes. You compile C, C++, Rust, Go or Zig to Wasm to run fast code safely on the web, at the edge and in plugins. It is how AI models such as Whisper and small LLMs run directly in the browser.

Prologue

Why learn WebAssembly?

WebAssembly lets you ship heavy computation, including AI models, to users' devices with no install and no server cost.

  • Fast: near-native speed, with SIMD and threads.
  • Portable and safe: sandboxed bytecode that runs anywhere.
  • On-device AI: run models privately in the browser with ONNX Runtime Web, Transformers.js or llama.cpp builds.
Prologue

Set up and run your first program

You rarely write Wasm by hand. Most people compile Rust (wasm-pack) or C/C++ (Emscripten). To read the text format, install WABT.

terminal
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
wasm-pack build --target web
src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, AI! Welcome to WebAssembly, {}.", name)
}
TipAdd wasm-bindgen to Cargo.toml and set crate-type = ["cdylib"].
Chapter 1

The text format (WAT)

Wasm has a readable text format. A module exports functions that JavaScript can call. Instructions push and pop values on a stack.

math.wat
(module
  (func $add (export "add") (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add)
  (func (export "fp16_bytes") (param $params f64) (result f64)
    local.get $params
    f64.const 2
    f64.mul))
Chapter 2

Loading Wasm from JavaScript

Compile WAT to a .wasm binary with wat2wasm, then instantiate it in the browser or Node.js and call its exports like normal functions.

main.js
// wat2wasm math.wat -o math.wasm
const { instance } = await WebAssembly.instantiateStreaming(fetch("math.wasm"));
console.log(instance.exports.add(2, 3));            // 5
console.log(instance.exports.fp16_bytes(8.03e9));   // 16060000000
Chapter 3

Linear memory

Each module has a linear memory: one big array of bytes shared with JavaScript. Passing arrays means writing them into this memory and passing a pointer.

sum.wat
(module
  (memory (export "mem") 1)          ;; 1 page = 64 KiB
  (func (export "sum") (param $ptr i32) (param $n i32) (result f32)
    (local $i i32) (local $acc f32)
    (block $done
      (loop $next
        (br_if $done (i32.ge_u (local.get $i) (local.get $n)))
        (local.set $acc (f32.add (local.get $acc)
          (f32.load (i32.add (local.get $ptr) (i32.mul (local.get $i) (i32.const 4))))))
        (local.set $i (i32.add (local.get $i) (i32.const 1)))
        (br $next)))
    (local.get $acc)))
Chapter 4

Compiling Rust to Wasm

wasm-bindgen generates the JavaScript glue, so you can pass strings and typed arrays directly.

src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn dot(a: &[f32], b: &[f32]) -> f32 {
    a.iter().zip(b).map(|(x, y)| x * y).sum()
}

// JavaScript:
// import init, { dot } from "./pkg/my_wasm.js";
// await init();
// dot(new Float32Array([1, 2, 3]), new Float32Array([4, 5, 6]));  // 32
Chapter 5

WASI and runtimes outside the browser

WASI gives Wasm safe access to files, clocks and networking, so the same binary runs on servers, edge platforms and as plugins.

terminal
# Build a Rust CLI for WASI and run it with Wasmtime
rustup target add wasm32-wasip1
cargo build --target wasm32-wasip1 --release
wasmtime target/wasm32-wasip1/release/app.wasm

# C with the WASI SDK
clang --target=wasm32-wasi -O2 main.c -o main.wasm
wasmtime main.wasm
Part II · AI with WebAssembly

AI with WebAssembly

WebAssembly is one of the main ways to run AI on users' devices. ONNX Runtime Web and Transformers.js use a Wasm backend (with SIMD and threads) and switch to WebGPU when available. llama.cpp and whisper.cpp compile to Wasm, and WebLLM combines Wasm with WebGPU to run LLMs fully in the browser.

The essential libraries

Transformers.jsHugging Face models in the browser (Wasm/WebGPU).
ONNX Runtime WebRun ONNX models with Wasm or WebGPU.
WebLLMLLMs in the browser with WebGPU.
wasm-bindgenRust and JavaScript interop.

Sentiment analysis in the browser

Transformers.js downloads a small model once and runs it locally with WebAssembly.

index.html
<script type="module">
import { pipeline } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.0";

const classify = await pipeline("sentiment-analysis",
  "Xenova/distilbert-base-uncased-finetuned-sst-2-english", { device: "wasm" });
console.log(await classify("Running AI in the browser is amazing!"));
// [{ label: "POSITIVE", score: 0.99 }]
</script>
Part III · Build an AI agent

A browser agent with WebLLM

WebLLM runs an LLM on the user's GPU through WebGPU, with WebAssembly for the runtime. Its OpenAI-style API means the agent loop is the same as on a server, except nothing leaves the device.

Your goal→Model plans→Tool call→Your code runs the tool→Result back to model→Final answer
agent.html
<script type="module">
import { CreateMLCEngine } from "https://esm.run/@mlc-ai/web-llm";

const engine = await CreateMLCEngine("Hermes-3-Llama-3.1-8B-q4f16_1-MLC",
  { initProgressCallback: p => console.log(p.text) });

const tools = [{ type: "function", function: { name: "get_time", description: "Current local time",
  parameters: { type: "object", properties: {} } } }];
const messages = [{ role: "user", content: "What time is it right now?" }];

for (let round = 0; round < 5; round++) {
  const msg = (await engine.chat.completions.create({ messages, tools })).choices[0].message;
  messages.push(msg);
  if (!msg.tool_calls?.length) { document.body.textContent = msg.content; break; }
  for (const call of msg.tool_calls) {
    messages.push({ role: "tool", tool_call_id: call.id, content: new Date().toLocaleTimeString() });
  }
}
</script>

Tool calling in WebLLM only works with models that support it, such as the Hermes series; check the WebLLM model list.

Run it locallyInstall Ollama, run ollama pull llama3.1, and keep it running. The code talks to its OpenAI-compatible API at http://localhost:11434/v1, so the same program also works with vLLM, llama.cpp server or a cloud API by changing the URL, model name and adding an API key.
Part IV · Your path

WebAssembly learning roadmap

Tick items off as you go. Your progress is saved in this browser.

Beginner

Intermediate

Advanced

Part IV · Your path

Projects to build

Image filterA Rust Wasm blur filter on a canvas.
Speech to textRun Whisper in the browser.
Private chatbotA WebLLM chat page that works offline.
Reference

WebAssembly cheat sheet

TaskSyntax
InstantiateWebAssembly.instantiateStreaming(fetch(u))
Call exportinstance.exports.add(1, 2)
Rust export#[wasm_bindgen] pub fn f()
Build Rustwasm-pack build --target web
WAT to binarywat2wasm a.wat -o a.wasm
Run WASIwasmtime app.wasm
Reference

Common mistakes

  • Copying large arrays between JavaScript and Wasm memory can cost more than the computation.
  • Browsers block .wasm loading from file:// URLs; use a local web server.
  • Threads need cross-origin isolation headers (COOP and COEP).
Reference

Where to go next

On TensorCore.si: Rust volume, JavaScript volume.

Keyboard: [ previous language · ] next language · / search the library