Skip to main content

🧩 WebAssembly Concepts

For years the browser could run exactly one language: JavaScript. WebAssembly changes that. It is a compact, portable binary format that lets code written in C, C++, Rust, Go, or AssemblyScript run inside the browser at close to native speed — right alongside your JavaScript, not in place of it. In this lesson you'll learn what WebAssembly really is, how source code becomes a .wasm binary, and how that binary runs safely inside a sandboxed virtual machine.

Week 14 · Day 5 (Friday: WebAssembly Basics) · Lecture 1

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Define WebAssembly as a portable binary instruction format and compile target
  • Trace the pipeline from a source language, through a compiler, to a .wasm binary running in the browser VM
  • Explain the stack-based execution model, linear memory, and why WASM is sandboxed and deterministic
  • Read a small WebAssembly text-format (.wat) module and match it to its binary
  • Describe how WASM complements JavaScript and decide when a task is a good fit
  • Name the main source languages and toolchains that target WebAssembly

Estimated Time: 55 minutes

Practice: Hand-trace a .wat add function and classify tasks as WASM-friendly or not.

In This Lesson

What Is WebAssembly?

WebAssembly (usually shortened to WASM) is a portable binary instruction format for a stack-based virtual machine. That's a mouthful, so let's unpack it one word at a time:

  • Binary — it ships as compact bytes (.wasm files), not human-readable text, so it downloads fast and parses quickly.
  • Instruction format — it is a low-level list of operations (add these two numbers, load this byte from memory) very close to what a CPU understands.
  • Portable — the same .wasm runs identically in every modern browser and in server runtimes, regardless of the operating system or chip.
  • Compile target — you don't usually write WASM by hand. You write C, C++, Rust, Go, or AssemblyScript and a compiler produces the .wasm for you.

📖 Analogy: WASM is a universal shipping container

Before standardized shipping containers, every port loaded cargo its own way. The container changed everything: pack goods once, and any crane, truck, or ship in the world can move the box without caring what's inside. WebAssembly is that container for code — a compiler packs your program into a standard box, and any browser or WASM runtime can execute it without knowing (or caring) that it started life as Rust or C++.

What WebAssembly is not

WebAssembly is not a replacement for JavaScript, and it is not a programming language you'll write day to day. It has no built-in access to the DOM, no garbage collector of its own (in its core form), and no way to make a network request on its own. It is a fast, safe execution engine that JavaScript drives. The two are partners: JS orchestrates the page and the browser APIs; WASM does the heavy number-crunching.

✅ The one-sentence definition

WebAssembly is a compact, portable binary format that runs compiled code (from C, C++, Rust, and others) at near-native speed inside the browser's sandboxed virtual machine, working alongside JavaScript.

The Compile Pipeline

The single most important mental model for WebAssembly is the pipeline: source languages → a compiler → a .wasm binary → the browser's virtual machine. Once you can picture this flow, everything else clicks into place.

Pipeline from source languages through a compiler to a wasm binary running in the browser virtual machine Source C / C++ Rust Go AssemblyScript Compiler Emscripten, wasm-pack… module.wasm portable binary Browser VM runs it
You write a high-level language; a toolchain compiles it to a portable .wasm binary; the browser's virtual machine loads and executes it.

The key insight: the "hard work" of translating human-friendly source into machine-friendly instructions happens once, at build time. JavaScript, by contrast, is delivered as text and must be parsed and just-in-time compiled by the browser every time the page loads. WebAssembly arrives pre-digested, which is a big part of why it starts fast and runs fast.

A brief history

WebAssembly grew out of earlier experiments to run fast code in the browser — Mozilla's asm.js (a strict, optimizable subset of JavaScript) and Google's Native Client. In 2015 the major browser vendors formed a community group, shipped a minimum viable product in 2017, and by 2019 WebAssembly was an official W3C recommendation with support in every mainstream browser. It is now a stable, standardized part of the web platform.

The Sandboxed Virtual Machine

When the browser loads a .wasm module, it runs the instructions on a small, well-defined virtual machine (VM). Understanding a few of its properties explains both why WASM is fast and why it is safe.

A stack-based machine

WebAssembly is stack-based: most instructions push values onto an operand stack or pop values off it. To add two numbers, you push the first, push the second, then run i32.add, which pops both and pushes the sum. This model is simple to validate and easy for the browser to translate into real machine code.

Linear memory: one big array of bytes

A WASM module gets a single, contiguous block of memory called linear memory — essentially a resizable ArrayBuffer. All of the module's data lives here, and crucially, JavaScript can see the exact same bytes through a typed array. This shared buffer is how larger data (like an image) crosses between the two worlds, a topic the next lesson explores in depth.

Sandboxed and deterministic by design

  • Sandboxed — a module can only touch its own linear memory. It cannot read arbitrary browser memory, reach the file system, or call the network unless JavaScript explicitly hands it a function to do so. A malicious or buggy .wasm is contained.
  • Structured control flow — there are no arbitrary "jump to any address" instructions. Branches are structured (blocks, loops, ifs), which lets the browser validate a module quickly and prove it is well-formed before running a single instruction.
  • Deterministic — given the same inputs, a WASM module produces the same outputs on every machine, with the same integer and floating-point behavior. There are no surprising platform differences.

💡 Why the sandbox matters

The web's whole security model rests on running untrusted code safely. WebAssembly was designed inside that model from day one: it validates fast, cannot escape its memory, and reaches the outside world only through explicit imports that JavaScript provides. You get native-like speed without giving up the browser's safety guarantees.

Binary & Text Formats

WebAssembly has two faces of the same thing. The binary format (.wasm) is what ships over the network. The text format (.wat, "WebAssembly Text") is a human-readable equivalent used for learning, debugging, and inspection. Tools like wat2wasm and wasm2wat convert between them.

The text format (.wat)

Here is a complete module that exports one function, add, taking two 32-bit integers and returning their sum. Read it top to bottom like a recipe:

(module
  ;; Define a function named $add that takes two i32 params
  ;; and returns one i32 result.
  (func $add (param $a i32) (param $b i32) (result i32)
    local.get $a      ;; push parameter a onto the stack
    local.get $b      ;; push parameter b onto the stack
    i32.add)          ;; pop both, push a + b

  ;; Make $add callable from JavaScript under the name "add".
  (export "add" (func $add)))

Notice the stack machine at work: local.get pushes a value, and i32.add consumes the top two values and leaves the result. The final value on the stack becomes the return value.

The binary format (.wasm)

Compiled to binary, that same module is just a stream of bytes. Viewed as hexadecimal it looks like this — unreadable to us, but tiny and fast for the browser:

00 61 73 6d  01 00 00 00   ; "\0asm" magic + version 1
01 07 01 60  02 7f 7f 01    ; Type section: (i32, i32) -> i32
7f 03 02 01  00 07 07 01    ; Function + Export sections
03 61 64 64  00 00 0a 09    ; "add" export, code section begins
01 07 00 20  00 20 01 6a 0b ; body: local.get 0, local.get 1, i32.add, end

The first eight bytes are always the same: the magic number \0asm followed by the version. After that come sections that describe the module's types, functions, memory, imports, exports, and the actual code bodies. You never write these bytes by hand — the compiler does — but seeing them demystifies what "a binary format" really means.

⚠️ You rarely touch .wat in real projects

The text format is a learning and debugging aid, not your daily workflow. In practice you write Rust or C++, run a build command, and get a .wasm plus a JavaScript "glue" file. Reading a little .wat is like reading a little assembly — invaluable for understanding, unnecessary for shipping.

WASM Alongside JavaScript

The whole point of WebAssembly on the web is that it complements JavaScript. Each does what it is best at. JavaScript owns the DOM, events, fetch, and the general glue of your app. WebAssembly owns the CPU-heavy inner loops. They call each other across a well-defined boundary.

graph LR JS["JavaScript
DOM, events, fetch"] -->|"calls exported functions"| WASM["WASM module
fast compute"] WASM -->|"returns numbers"| JS WASM -->|"calls imported functions"| IMP["Imported JS functions"] IMP --> JS JS -->|"reads and writes"| MEM["Shared linear memory"] WASM -->|"reads and writes"| MEM

Two directions of interop matter:

  • JS calls WASM — after loading a module you get an instance.exports object. Its exported functions are ordinary-looking JavaScript functions that happen to run compiled code.
  • WASM calls JS — you can hand the module an import object full of JavaScript functions. Since WASM has no DOM access of its own, this is how it reaches back out (for example, to log a value or touch the page).
// The simplest possible load-and-call in the browser.
// instantiateStreaming compiles the module *while it downloads*.
const { instance } = await WebAssembly.instantiateStreaming(
  fetch('add.wasm')
);

// instance.exports.add is our compiled function.
console.log(instance.exports.add(40, 2)); // 42

💡 The boundary has a cost

Plain numbers (i32, i64, f32, f64) cross the JS/WASM boundary almost for free. Strings, arrays, and objects do not — they must be copied into or read out of linear memory, which is called marshalling. Cheap numbers and expensive structured data is the central trade-off you'll design around, and the next two lessons dig into it.

Source Languages & Tooling

You reach WebAssembly through a language you already know or can learn quickly. The most common paths:

LanguageToolchainBest for
C / C++EmscriptenPorting large existing codebases and libraries
Rustwasm-pack + wasm-bindgenNew, memory-safe, high-performance modules
AssemblyScriptasc compilerWeb devs — a TypeScript-like syntax compiling to WASM
GoOfficial GOOS=js GOARCH=wasm targetReusing Go logic (larger binaries)

Rust: the ergonomic modern path

Rust has become the flagship WebAssembly language because wasm-bindgen automates the tedious marshalling. You write ordinary Rust; the tooling generates the JS glue that passes strings and structs across the boundary for you.

// lib.rs — a Rust function exported to JavaScript.
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
    if n <= 1 {
        return n;
    }
    let (mut a, mut b) = (0u32, 1u32);
    for _ in 2..=n {
        let next = a + b;   // iterative — no deep recursion needed
        a = b;
        b = next;
    }
    b
}
// Build with: wasm-pack build --target web
// wasm-pack emits a .wasm plus a JS module that wraps it.
import init, { fibonacci } from './pkg/my_module.js';

await init();                 // loads and instantiates the .wasm
console.log(fibonacci(10));   // 55

C with Emscripten

Emscripten is the veteran toolchain for C and C++. It can compile an entire game engine's worth of code, emitting a .wasm plus a JavaScript file that sets up memory and glue:

# fib.c compiled to WASM:
#   emcc fib.c -o fib.js -sEXPORTED_FUNCTIONS=_fibonacci -sEXPORTED_RUNTIME_METHODS=cwrap
#
# Then in the browser, load the generated fib.js which sets up
# Module, and call the wrapped function:
const fibonacci = Module.cwrap('fibonacci', 'number', ['number']);
console.log(fibonacci(10)); // 55

✅ Which should you pick?

Porting existing C/C++? Reach for Emscripten. Writing something new and want safety plus great tooling? Choose Rust + wasm-pack. Coming straight from web development and want the gentlest ramp? Try AssemblyScript, whose syntax will feel like TypeScript.

Beyond the Browser (WASI)

WebAssembly was born in the browser, but its portability and sandbox make it valuable everywhere. The WebAssembly System Interface (WASI) is a standard that lets a .wasm module request system capabilities — files, clocks, randomness — in a controlled, capability-based way outside the browser.

graph TD MOD["WASM module"] --> WASI{"WASI runtime"} WASI --> FS["File system access"] WASI --> NET["Network sockets"] WASI --> CLK["Clock and time"] WASI --> RND["Random numbers"]

Because a WASI runtime grants only the capabilities you explicitly allow, WebAssembly has become popular for:

  • Serverless and edge computing — modules start in microseconds and are isolated, so platforms run untrusted customer code close to users.
  • Plugin systems — applications embed a WASM runtime to run third-party extensions safely inside a sandbox.
  • Portable server workloads — one binary runs on any OS or chip with a compatible runtime.

📖 The takeaway

"WebAssembly" no longer means "assembly for the web" exclusively. It is becoming a universal, secure, portable compilation target for the browser, the server, the edge, and embedded devices — anywhere you want to run fast, sandboxed code.

Practice & Quiz

🏋️ Exercise 1: Trace the stack machine

Goal: Given the .wat below, work out what value the exported function returns when called as calc(7, 3). Track the operand stack instruction by instruction.

(module
  (func $calc (param $a i32) (param $b i32) (result i32)
    local.get $a     ;; stack: [a]
    local.get $b     ;; stack: [a, b]
    i32.sub          ;; stack: [a - b]
    local.get $b     ;; stack: [a - b, b]
    i32.mul)         ;; stack: [(a - b) * b]
  (export "calc" (func $calc)))
💡 Hint

Each local.get pushes a value. i32.sub pops the top two and pushes second_from_top - top. i32.mul pops two and pushes their product. Substitute a = 7, b = 3 and compute step by step.

✅ Solution

The function computes (a - b) * b. With a = 7, b = 3: (7 - 3) * 3 = 4 * 3 = 12. In JavaScript, instance.exports.calc(7, 3) returns 12.

🏋️ Exercise 2: WASM-friendly or not?

Goal: For each task, decide whether WebAssembly is likely to help or is a poor fit, and say why in one line.

  1. Applying a Gaussian blur to a 4000×3000 photo
  2. Toggling a CSS class when a button is clicked
  3. Decoding a custom video codec frame by frame
  4. Reading three form fields and validating an email address
💡 Hint

Ask: is this task CPU-heavy with lots of arithmetic over big buffers of numbers, or is it DOM-heavy / trivial? Remember the boundary cost — tiny jobs pay overhead without a payoff.

✅ Solution
  • 1. Helps. Heavy per-pixel arithmetic over millions of numbers — WASM's sweet spot.
  • 2. Poor fit. Pure DOM work; WASM has no DOM access and the interop overhead dwarfs the task.
  • 3. Helps. Tight numeric inner loops on large buffers — classic codec territory.
  • 4. Poor fit. Trivial, string-heavy, DOM-adjacent work; plain JavaScript is simpler and just as fast.

🎯 Quick Quiz

Question 1: Which statement best describes WebAssembly?

Question 2: Why can WebAssembly not directly change the page's DOM?

Question 3: Which data crosses the JS/WASM boundary most cheaply?

Best Practices & Pitfalls

✅ Do

  • Reach for WASM when work is CPU-heavy: image/video, crypto, codecs, physics, simulation
  • Keep JavaScript in charge of the DOM, events, and fetch — let it orchestrate
  • Design your API so numbers cross the boundary and bulk data lives in shared memory
  • Serve .wasm with the application/wasm MIME type so streaming compilation works
  • Learn a little .wat to understand and debug, then let the compiler produce the binary

❌ Don't

  • Reach for WASM for trivial or DOM-heavy tasks — the interop overhead can exceed the gain
  • Expect WASM to touch the DOM or network on its own — it needs JS imports
  • Assume WASM is always faster than JavaScript — measure before you commit
  • Hand-write .wasm bytes — use a real toolchain (Emscripten, wasm-pack, AssemblyScript)

⚠️ "Near-native," not "native"

WebAssembly runs fast, but it still lives inside the browser's VM and its sandbox. Expect roughly native-adjacent performance for compute, not literal bare-metal speed — and remember that crossing into and out of WASM costs a little every time.

Summary

🎉 Key Takeaways

  • WebAssembly is a portable binary instruction format and a compile target for C, C++, Rust, Go, and AssemblyScript
  • The pipeline is source → compiler → .wasm → browser VM; the hard translation happens once, at build time
  • It runs on a sandboxed, stack-based VM with linear memory, is deterministic, and validates fast
  • It has both a binary format (.wasm, shipped) and a text format (.wat, for humans)
  • WASM complements JavaScript: JS drives the DOM, WASM crunches numbers — numbers cross cheaply, structured data needs marshalling
  • With WASI, WebAssembly runs beyond the browser on servers, the edge, and in plugin systems

📚 Additional Resources

🚀 What's Next?

Now that you understand what WebAssembly is, the next lesson gets hands-on: Using WASM in Web Apps — loading modules with instantiateStreaming, wiring up imports and exports, and marshalling strings and image data across shared linear memory.

🎉 Solid foundation!

You can now explain WebAssembly to a teammate and reason about when it belongs in a project. Time to put a module to work.