Zig WebAssembly

WebAssembly (Wasm) is a binary instruction format that runs in browsers and server-side runtimes like Node.js, Deno, and Wasmtime. Zig compiles to WebAssembly natively — no extra toolchain, no Emscripten, no LLVM wrapper needed. A single zig build command produces a .wasm binary ready for the browser or any Wasm host.

Why Zig + WebAssembly

  Zig advantages for Wasm:
  ✓ No garbage collector → no unpredictable pauses
  ✓ No standard library dependency (freestanding target)
  ✓ Tiny output binary (no runtime overhead)
  ✓ Direct control over exported functions
  ✓ Cross-compiles from any platform to Wasm
  ✓ Works with any Wasm host: browser, Node.js, Wasmtime

Build Target

  Wasm targets in Zig:
  wasm32-freestanding       ← no OS, no libc (for browsers)
  wasm32-wasi               ← WASI (WebAssembly System Interface)
  wasm64-freestanding       ← 64-bit Wasm (experimental)

  Build command:
  zig build-lib src/math.zig \
    -target wasm32-freestanding \
    -dynamic \
    -rdynamic \
    -O ReleaseFast

A Simple Wasm Library

// src/math.zig
// export keyword makes these visible to JavaScript

export fn add(a: i32, b: i32) i32 {
    return a + b;
}

export fn fibonacci(n: u32) u32 {
    if (n <= 1) return n;
    var a: u32 = 0;
    var b: u32 = 1;
    var i: u32 = 2;
    while (i <= n) : (i += 1) {
        const c = a + b;
        a = b;
        b = c;
    }
    return b;
}

export fn factorial(n: u32) u64 {
    var result: u64 = 1;
    var i: u32 = 2;
    while (i <= n) : (i += 1) {
        result *= i;
    }
    return result;
}
  Zig source
       │
  zig build-lib -target wasm32-freestanding
       │
  math.wasm  ← binary Wasm file (~1KB for these functions)
       │
  Browser / Node.js loads and runs it

Using the Wasm Module in JavaScript

// index.html / script.js
async function loadWasm() {
    const response  = await fetch('math.wasm');
    const bytes     = await response.arrayBuffer();
    const { instance } = await WebAssembly.instantiate(bytes, {});
    const exports   = instance.exports;

    console.log('add(3, 4)       =', exports.add(3, 4));       // 7
    console.log('fibonacci(10)   =', exports.fibonacci(10));   // 55
    console.log('factorial(10)   =', exports.factorial(10));   // 3628800
}

loadWasm();
  Browser
    │
  fetch('math.wasm')
    │
  WebAssembly.instantiate()
    │
  instance.exports.add(3, 4) → 7
  instance.exports.fibonacci(10) → 55

Working with Memory — Passing Arrays

Wasm and JavaScript share a flat linear memory buffer. To pass arrays and strings between them, Zig writes data into its memory and JavaScript reads from the same buffer:

// src/buffer.zig
var wasm_memory: [65536]u8 = undefined;

export fn getMemoryPtr() [*]u8 {
    return &wasm_memory;
}

export fn sumArray(len: u32) f64 {
    var total: f64 = 0;
    var i: u32 = 0;
    while (i < len) : (i += 1) {
        total += @as(f64, @floatFromInt(wasm_memory[i]));
    }
    return total;
}
// JavaScript side:
const memPtr = exports.getMemoryPtr();
const memory = new Uint8Array(instance.exports.memory.buffer);

// Write data into Wasm memory
const data = [10, 20, 30, 40, 50];
for (let i = 0; i < data.length; i++) {
    memory[memPtr + i] = data[i];
}

// Call Zig function to sum it
const total = exports.sumArray(data.length);
console.log('Sum:', total);  // 150
  Shared memory model:
  ┌──────────────────────────────────────────────────────┐
  │ WebAssembly Linear Memory (single flat buffer)       │
  │ [0][1][2]...[memPtr][memPtr+1]...[65535]             │
  │                ↑                                     │
  │   Zig's wasm_memory array starts here                │
  │   JavaScript writes: [10,20,30,40,50]                │
  │   Zig reads and sums: 150                            │
  └──────────────────────────────────────────────────────┘

WASI — WebAssembly System Interface

WASI gives Wasm programs sandboxed access to the file system, environment variables, stdin/stdout, and clocks. Use it for server-side Wasm or command-line tools that run in a Wasm runtime:

// src/hello.zig — WASI target can use std library
const std = @import("std");

pub fn main() void {
    std.debug.print("Hello from Wasm + WASI!\n", .{});
}
  Build:
  zig build-exe src/hello.zig -target wasm32-wasi

  Run with wasmtime:
  wasmtime hello.wasm
  Output: Hello from Wasm + WASI!

  Run with Node.js (WASI support):
  node --experimental-wasi-unstable-preview1 run.mjs

build.zig for a Wasm Library

const std = @import("std");

pub fn build(b: *std.Build) void {
    const lib = b.addSharedLibrary(.{
        .name   = "mylib",
        .root_source_file = b.path("src/lib.zig"),
        .target = b.resolveTargetQuery(.{
            .cpu_arch = .wasm32,
            .os_tag   = .freestanding,
        }),
        .optimize = .ReleaseSmall,  // smallest binary for web delivery
    });

    // Export all pub functions
    lib.rdynamic = true;
    b.installArtifact(lib);
}

Minimizing Binary Size

  Techniques for small Wasm output:
  ✓ Use -O ReleaseSmall
  ✓ Avoid importing std library (freestanding target)
  ✓ Use @export only for functions JavaScript calls
  ✓ Strip debug info: --strip
  ✓ Run wasm-opt (Binaryen tool) for further reduction

  Typical sizes:
  Small math library (no std): 1–5 KB
  Full application with std:   50–200 KB
  C equivalent with Emscripten: 300–1000 KB

Zig Wasm vs Alternatives

  Language     │ Wasm output │ GC overhead │ Build complexity
  ─────────────┼─────────────┼─────────────┼─────────────────
  Zig          │ Tiny        │ None        │ One command
  Rust         │ Small       │ None        │ Moderate
  C/Emscripten │ Moderate    │ None        │ Complex
  Go           │ Large       │ Yes         │ Moderate
  Java/Kotlin  │ Large       │ Yes         │ Complex

Zig produces some of the smallest Wasm binaries of any language, especially when targeting wasm32-freestanding without the standard library. The direct-to-Wasm compilation path with no intermediate tools makes Zig attractive for performance-critical web modules, game engines, and plugin systems that run inside browsers or server Wasm runtimes.

Leave a Comment

Your email address will not be published. Required fields are marked *