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.
