A student’s field guide · 01
WASM: A Small Machine for the Web
What WebAssembly is, what it does at runtime, and how to think about using it as a software engineer.
01 — The idea
WebAssembly, usually called WASM, is a compact binary instruction format designed to run code safely and predictably in a virtual machine.
It began as a way to bring languages such as C, C++, and Rust to the browser without translating every program into JavaScript. Today, WebAssembly can also run outside browsers in servers, command-line tools, databases, plugins, and edge systems.
WASM is not a programming language like Rust or C. It is a compilation target: a low-level format that another language can be compiled into.
Why not use JavaScript for everything?
JavaScript is an excellent high-level language, but some workloads benefit from predictable low-level execution. Image processing, audio codecs, physics, encryption, scientific calculations, and games may already exist as optimized native libraries.
WASM lets engineers reuse those libraries or write performance-sensitive components in a language suited to them, while keeping the surrounding application in JavaScript, TypeScript, or another host language.
02 — The runtime
A WASM module is commonly a .wasm binary. The binary contains functions, memory descriptions, optional tables, and metadata. A host loads the module, checks it, creates an executable instance, and calls its exported functions.
Compile, instantiate, call
- Compile. The host turns the binary into a form suitable for the current machine. Browsers may do this ahead of time or while loading.
- Instantiate. The host connects the module to imported functions, memory, or other resources. This is where the module’s environment is defined.
- Call. JavaScript or another host invokes an exported WASM function and receives its result.
A small machine, not a tiny operating system
WASM instructions operate on numbers and linear memory. A module does not automatically get access to your files, DOM, network, or operating-system calls. It must receive those abilities through explicit imports supplied by the host.
This separation is central to its safety model. A downloaded module can calculate, but it cannot simply inspect the user’s machine. The host decides which doors are open.
Linear memory
WASM memory is a contiguous, growable byte array. In a browser, JavaScript can view that memory through typed arrays. When a function needs a string or a complex object, the two sides usually agree on a memory layout: a pointer, a length, and bytes stored at that location.
This is powerful but less convenient than passing ordinary JavaScript objects. Crossing the boundary has a cost, so useful designs make relatively substantial calls rather than thousands of tiny ones.
03 — The engineering view
Think of WASM as a component inside a larger system. It is rarely “the whole application.” A typical architecture uses a high-level host for orchestration and a WASM module for a focused computation.
What WASM is good at
- Portable, repeatable computation across browsers and operating systems.
- Reusing existing native or systems-language libraries.
- CPU-heavy work that would otherwise block a user interface.
- Running untrusted or semi-trusted components with restricted capabilities.
- Sharing one computational core between a browser app, a server, and a command-line tool.
What it does not solve automatically
- It does not make every program faster; data copying and boundary calls matter.
- It does not replace JavaScript APIs for the DOM and most browser interaction.
- It does not provide a universal package or application runtime by itself.
- It does not remove the need to understand memory ownership, errors, and versioning.
Security and trust
Validation and isolation are related but different. The WASM binary is validated before execution, and its ordinary memory is separate from the host’s memory. But a module can still contain bugs, consume excessive CPU, or misuse any capability the host grants it.
Use resource limits, input validation, timeouts, and carefully chosen imports when running code you do not fully trust.
04 — A real, small example
Suppose you are building a browser-based text tool. It needs to count the number of words in a large document whenever the user edits it. This is simple enough to understand, but large enough to demonstrate a useful split.
The contract
The host sends text to the module. The module counts words and returns one integer. The host remains responsible for the text box and the page; WASM owns the counting loop.
// Conceptual Rust module
#[no_mangle]
pub extern "C" fn count_words(ptr: *const u8, len: usize) -> u32 {
let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
let mut count = 0;
let mut inside_word = false;
for &byte in bytes {
let is_space = byte == b' ' || byte == b'\n' || byte == b'\t';
if is_space {
inside_word = false;
} else if !inside_word {
count += 1;
inside_word = true;
}
}
count
}
The exact compiler setup is outside this first example, but the important idea is the exported function: it accepts a memory address and a byte length. A production version would handle Unicode word boundaries, invalid UTF-8, and memory ownership more carefully.
Calling it from a browser
const response = await fetch("counter.wasm");
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes, {});
const countWords = instance.exports.count_words;
// A real adapter would copy UTF-8 text into the module's memory first.
const result = countWords(pointer, byteLength);
document.querySelector("#count").textContent = result;
The missing adapter is not a footnote; it is the main engineering seam. It allocates space in WASM memory, encodes the JavaScript string as UTF-8, copies the bytes into that space, calls the function, and eventually reclaims the space. Tools such as language-specific WASM bindings can generate much of this glue.
A sensible implementation path
- Start with a pure JavaScript word counter and write tests for the behavior you want.
- Implement the same algorithm in Rust, C, or another language that can target WASM.
- Export one narrow function and compile a module for your chosen runtime.
- Write the adapter that moves bytes across the boundary.
- Benchmark realistic document sizes, including the cost of copying and calling.
- Keep the WASM version only if it improves speed, portability, reuse, or isolation enough to justify its complexity.
Glossary
- Module
- The compiled WASM artifact: code, declarations, and data that can be loaded by a host.
- Instance
- A live module connected to specific imports and memory.
- Import
- A function, memory, table, or value provided by the host to the module.
- Export
- A function or resource exposed by the module for the host to use.
- Linear memory
- The module’s byte-addressable memory region, separate from ordinary host objects.
Where to go next
To deepen your understanding, learn one toolchain end to end: compile a small Rust or C function, inspect its exports, load it in a browser, and measure it against a host-language version.
Useful subjects to study next: WASI for non-browser system interfaces, component models for stronger interfaces, JavaScript typed arrays, memory ownership, and profiling across a host–WASM boundary.