From machine code to browser bytecode

Assembly language sits at the bottom of the programming stack: each instruction maps directly to a machine-code operation of a specific processor architecture. The MUL instruction on x86 systems, for example, takes an implied destination operand in register AX, multiplies it by a source operand such as the value in register CX, and stores the result back in AX. The equivalent task in JavaScript is straightforward:

const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;

Low-level code like this runs far more efficiently than human-readable high-level code, but it is tied to a single architecture. That raises an obvious question: could the performance of assembly be ported to a form that runs anywhere? That question ultimately led to WebAssembly.

asm.js: the architectural detour

The first answer came as asm.js, a strict subset of JavaScript engineered as a compilation target for memory-unsafe languages such as C and C++. Instead of adding new browser features, asm.js described a sandboxed virtual machine within the semantics of JavaScript itself. Compilers like early Emscripten (built on LLVM) translated statically typed source code into this subset, and engines could then apply ahead-of-time (AOT) optimization to valid asm.js code, which improved performance by restricting supported language features to those amenable to such optimization.

Mozilla shipped support in Firefox 22 under the name OdinMonkey, and Chrome eventually followed in version 61. asm.js remains functional as a fallback for environments without WebAssembly, but it was superseded once browsers converged on a purpose-built format.

WebAssembly fundamentals

WebAssembly is a low-level, assembly-like language that ships as a compact binary format and executes at near-native speed. Its primary purpose is to serve as a compilation target for languages like C, C++, and Rust, letting existing codebases run in the browser alongside JavaScript. The component-model work extends support to memory-managed languages as well; Kotlin/Wasm is already available, with support for Java and Dart in progress.

Browsers are not the only place WebAssembly runs. The WASI (WebAssembly System Interface) defines a modular, portable, and security-focused system interface that lets the same bytecode execute in non-browser runtimes in a sandboxed environment.

Conceptually, execution uses a program counter that advances through instructions on a stack-based virtual machine. Instructions are either control instructions, which form control constructs, pop arguments from the stack, and may alter the program counter; or simple instructions, which pop arguments, apply an operator, push results, and implicitly advance the program counter. In practice, engines rarely use the virtual machine directly; they compile the bytecode to machine code before execution.

The WebAssembly sample below does what the earlier x86 example did, in a portable format:

i32.const 5  ; Push the integer value 5 onto the stack.
i32.const 10 ; Push the integer value 10 onto the stack.
i32.mul      ; Pop the two most recent items on the stack,
             ; multiply them, and push the result onto the stack.

Announced in 2015 and first released in March 2017, WebAssembly became a W3C recommendation on December 5, 2019, and is supported in all major browsers.

Two representations, one format

WebAssembly has a textual form and a binary form, which are two sides of the same coin.

The textual representation uses S-expressions and the .wat file extension (short for WebAssembly text format). It is possible, though hardly pleasant, to write it by hand. The earlier multiplication example becomes more practical with factors as parameters:

(module
  (func $mul (param $factor1 i32) (param $factor2 i32) (result i32)
    local.get $factor1
    local.get $factor2
    i32.mul)
  (export "mul" (func $mul))
)

The binary representation, using the .wasm extension, is not intended for human authors. Tools like wat2wasm convert text to binary, adding comments for readability where the raw binary would otherwise be opaque:

0000000: 0061 736d                             ; WASM_BINARY_MAGIC
0000004: 0100 0000                             ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                    ; section code
0000009: 00                                    ; section size (guess)
000000a: 01                                    ; num types
; func type 0
000000b: 60                                    ; func
000000c: 02                                    ; num params
000000d: 7f                                    ; i32
000000e: 7f                                    ; i32
000000f: 01                                    ; num results
0000010: 7f                                    ; i32
0000009: 07                                    ; FIXUP section size
; section "Function" (3)
0000011: 03                                    ; section code
0000012: 00                                    ; section size (guess)
0000013: 01                                    ; num functions
0000014: 00                                    ; function 0 signature index
0000012: 02                                    ; FIXUP section size
; section "Export" (7)
0000015: 07                                    ; section code
0000016: 00                                    ; section size (guess)
0000017: 01                                    ; num exports
0000018: 03                                    ; string length
0000019: 6d75 6c                          mul  ; export name
000001c: 00                                    ; export kind
000001d: 00                                    ; export func index
0000016: 07                                    ; FIXUP section size
; section "Code" (10)
000001e: 0a                                    ; section code
000001f: 00                                    ; section size (guess)
0000020: 01                                    ; num functions
; function body 0
0000021: 00                                    ; func body size (guess)
0000022: 00                                    ; local decl count
0000023: 20                                    ; local.get
0000024: 00                                    ; local index
0000025: 20                                    ; local.get
0000026: 01                                    ; local index
0000027: 6c                                    ; i32.mul
0000028: 0b                                    ; end
0000021: 07                                    ; FIXUP func body size
000001f: 09                                    ; FIXUP section size
; section "name"
0000029: 00                                    ; section code
000002a: 00                                    ; section size (guess)
000002b: 04                                    ; string length
000002c: 6e61 6d65                       name  ; custom section name
0000030: 01                                    ; name subsection type
0000031: 00                                    ; subsection size (guess)
0000032: 01                                    ; num names
0000033: 00                                    ; elem index
0000034: 03                                    ; string length
0000035: 6d75 6c                          mul  ; elem name 0
0000031: 06                                    ; FIXUP subsection size
0000038: 02                                    ; local name type
0000039: 00                                    ; subsection size (guess)
000003a: 01                                    ; num functions
000003b: 00                                    ; function index
000003c: 02                                    ; num locals
000003d: 00                                    ; local index
000003e: 07                                    ; string length
000003f: 6661 6374 6f72 31            factor1  ; local name 0
0000046: 01                                    ; local index
0000047: 07                                    ; string length
0000048: 6661 6374 6f72 32            factor2  ; local name 1
0000039: 15                                    ; FIXUP subsection size
000002a: 24                                    ; FIXUP section size

Compiler-driven workflow

Because neither text nor binary layout appeals to humans, real-world projects use a compiler such as Emscripten to translate from higher-level languages. Starting from a simple C program:

#include <stdio.h>

int main() {
  printf("Hello World\n");
  return 0;
}

the standard approach compiles with gcc:

$ gcc hello.c -o hello

With Emscripten installed, the same source compiles to WebAssembly using emcc and nearly identical arguments:

$ emcc hello.c -o hello.html

That invocation yields hello.wasm plus the HTML wrapper hello.html; serving that file shows "Hello World" in the DevTools console. Alternatively, a wrapperless build produces only the WebAssembly file and a JavaScript shim:

$ emcc hello.c -o hello.js

The resulting hello.js can be run directly under Node.js:

$ node hello.js
Hello World

Where to go next

WebAssembly is a deep topic, and its tooling and language ecosystem evolve quickly. The MDN WebAssembly documentation covers the format, and the Emscripten documentation walks through compilation workflows. For developers more accustomed to front-end work than to systems languages, the initial learning curve can resemble the famous "how to draw an owl" joke. Practitioners who get stuck often find good answers by asking nicely on Stack Overflow's webassembly tag.