WebAssembly Guide: Run C, Rust & Go in the Browser (2026)

A practical introduction to WebAssembly. Compile C, Rust, and Go to WASM, call it from JavaScript, and learn when WASM is the right tool for the job. Code examples you can run today, real-world use cases, and honest performance comparisons.

In This Guide
  1. What Is WebAssembly and Why It Matters
  2. How WASM Works: Compilation, Execution, Memory
  3. Your First WASM Module: C to WASM with Emscripten
  4. Rust + WASM: wasm-pack and wasm-bindgen
  5. Go + WASM: TinyGo for Smaller Binaries
  6. JavaScript and WASM Interop
  7. Real-World Use Cases
  8. Performance: WASM vs JavaScript Benchmarks
  9. WASI and Server-Side WASM
  10. Tools and Debugging
  11. Related Developer Tools
  12. Frequently Asked Questions

WebAssembly (WASM) lets you run code written in C, Rust, Go, and other compiled languages inside a web browser at near-native speed. It is not a replacement for JavaScript. It is a compilation target that sits alongside JavaScript, handling the work that JavaScript was never designed to do well: heavy computation, tight loops, and memory-intensive operations.

Every major browser supports WebAssembly. Chrome, Firefox, Safari, and Edge have shipped stable WASM support since 2017. As of 2026, WASM runs on over 95% of browsers worldwide. This guide takes you from zero to building and shipping WASM modules with practical code you can copy and run.

Convert between binary and other formats. When working with WASM binary files, the Binary Converter helps you inspect and convert binary, hex, and decimal values directly in your browser.

What Is WebAssembly and Why It Matters

WebAssembly is a binary instruction format designed as a portable compilation target. Think of it as a virtual processor that every browser implements. When you write a function in C or Rust and compile it to WASM, the browser translates those WASM instructions into native machine code for the user's actual CPU.

Three properties make WASM significant for web developers:

What WASM is not: it is not a way to run arbitrary native code. It is not a replacement for JavaScript for building UIs. It is not faster than JavaScript for everything. It is a tool for compute-heavy tasks where JavaScript's dynamic nature creates overhead.

How WASM Works: Compilation, Execution, Memory

The Compilation Pipeline

Source code goes through two compilation steps:

  1. Ahead-of-time (AOT) compilation: A compiler (Emscripten for C/C++, wasm-pack for Rust, TinyGo for Go) transforms source code into a .wasm binary file. This file contains typed instructions in a compact binary format.
  2. Browser compilation: The browser's WASM engine compiles the binary into native machine code for the host CPU. This happens at load time and is typically fast because WASM's type system makes the translation straightforward.

The .wasm file is smaller and faster to parse than equivalent JavaScript because it is a dense binary format, not text. A WASM module that performs the same work as 100KB of minified JavaScript might be 30-50KB as a .wasm file.

The Memory Model

WASM uses linear memory: a contiguous, resizable array of bytes. This is fundamentally different from JavaScript's garbage-collected heap. Your WASM code reads and writes to specific byte offsets in this array. There is no garbage collector, no object headers, and no hidden classes.

// JavaScript side: create memory for the WASM module
const memory = new WebAssembly.Memory({
  initial: 256,  // 256 pages = 16MB (each page is 64KB)
  maximum: 512   // can grow up to 32MB
});

// Access memory as different typed views
const buffer = new Uint8Array(memory.buffer);
const floats = new Float64Array(memory.buffer);

This shared memory model is how JavaScript and WASM exchange data. Instead of copying objects back and forth, both sides read and write the same byte array.

Imports and Exports

A WASM module declares what it needs (imports) and what it provides (exports). Imports are JavaScript functions the WASM code can call. Exports are WASM functions that JavaScript can call. This is the interface boundary.

// Instantiate a WASM module with imports
const importObject = {
  env: {
    log_value: (n) => console.log('WASM says:', n),
    get_time: () => Date.now()
  }
};

const { instance } = await WebAssembly.instantiateStreaming(
  fetch('module.wasm'),
  importObject
);

// Call an exported WASM function
const result = instance.exports.calculate(42);

Your First WASM Module: C to WASM with Emscripten

Emscripten is the oldest and most mature WASM toolchain. It compiles C and C++ to WebAssembly and generates the JavaScript glue code for loading and calling the module.

Install Emscripten

Terminal
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh

Write a C Function

math.c
#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int fibonacci(int n) {
    if (n <= 1) return n;
    int a = 0, b = 1;
    for (int i = 2; i <= n; i++) {
        int temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}

EMSCRIPTEN_KEEPALIVE
double sum_array(double* arr, int len) {
    double total = 0.0;
    for (int i = 0; i < len; i++) {
        total += arr[i];
    }
    return total;
}

The EMSCRIPTEN_KEEPALIVE macro prevents the compiler from eliminating these functions during dead code elimination.

Compile to WASM

Terminal
emcc math.c -o math.js \
  -s EXPORTED_FUNCTIONS='["_fibonacci", "_sum_array"]' \
  -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \
  -O3

This produces math.wasm (the binary) and math.js (the loader). The -O3 flag enables aggressive optimizations including function inlining and dead code elimination.

Call from JavaScript

index.html
<script src="math.js"></script>
<script>
  Module.onRuntimeInitialized = () => {
    // Direct call
    const fib20 = Module.ccall('fibonacci', 'number', ['number'], [20]);
    console.log('fib(20) =', fib20); // 6765

    // Wrapped for repeated calls
    const fibonacci = Module.cwrap('fibonacci', 'number', ['number']);
    console.log('fib(30) =', fibonacci(30)); // 832040
    console.log('fib(40) =', fibonacci(40)); // 102334155
  };
</script>
Output Size Tip

Add -s MINIMAL_RUNTIME=1 and --closure 1 to strip the JavaScript glue code down to the minimum. For a simple math module, this can reduce the JS file from 50KB to under 5KB.

Rust + WASM: wasm-pack and wasm-bindgen

Rust is the most popular language for new WASM projects. It produces the smallest binaries, has no runtime garbage collector, and the tooling is excellent. The wasm-pack tool handles compilation, optimization, and npm package generation in one command.

Setup

Terminal
# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Add the WASM target
rustup target add wasm32-unknown-unknown

# Install wasm-pack
cargo install wasm-pack

Create a WASM Library

Terminal
cargo new --lib wasm-image-filter
cd wasm-image-filter
Cargo.toml
[package]
name = "wasm-image-filter"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

[profile.release]
opt-level = "z"     # optimize for size
lto = true          # link-time optimization
strip = true        # strip debug symbols
src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn grayscale(pixels: &mut [u8]) {
    // pixels is RGBA data: [r, g, b, a, r, g, b, a, ...]
    for chunk in pixels.chunks_exact_mut(4) {
        let r = chunk[0] as f32;
        let g = chunk[1] as f32;
        let b = chunk[2] as f32;
        // Luminance formula (ITU-R BT.709)
        let gray = (0.2126 * r + 0.7152 * g + 0.0722 * b) as u8;
        chunk[0] = gray;
        chunk[1] = gray;
        chunk[2] = gray;
        // chunk[3] (alpha) stays unchanged
    }
}

#[wasm_bindgen]
pub fn brightness(pixels: &mut [u8], factor: f32) {
    for chunk in pixels.chunks_exact_mut(4) {
        chunk[0] = (chunk[0] as f32 * factor).min(255.0) as u8;
        chunk[1] = (chunk[1] as f32 * factor).min(255.0) as u8;
        chunk[2] = (chunk[2] as f32 * factor).min(255.0) as u8;
    }
}

#[wasm_bindgen]
pub fn invert(pixels: &mut [u8]) {
    for chunk in pixels.chunks_exact_mut(4) {
        chunk[0] = 255 - chunk[0];
        chunk[1] = 255 - chunk[1];
        chunk[2] = 255 - chunk[2];
    }
}

Build and Use

Terminal
wasm-pack build --target web --release
JavaScript
import init, { grayscale, brightness, invert }
  from './pkg/wasm_image_filter.js';

await init();

// Get pixel data from a canvas
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

// Apply grayscale filter in WASM (operates on the pixel buffer directly)
grayscale(imageData.data);
ctx.putImageData(imageData, 0, 0);

The wasm-bindgen crate generates all the JavaScript glue code. The &mut [u8] parameter maps directly to a JavaScript Uint8Array, sharing memory without copying. For a 1920x1080 image, that is 8.3 million bytes processed in place.

Go + WASM: TinyGo for Smaller Binaries

Standard Go can compile to WASM, but the output includes the entire Go runtime and garbage collector, resulting in binaries of 5-15MB. TinyGo is an alternative Go compiler that produces much smaller WASM binaries, typically 100KB-1MB, by using a different runtime optimized for constrained environments.

Standard Go (Large Binary)

main.go
package main

import (
    "syscall/js"
)

func fibonacci(this js.Value, args []js.Value) interface{} {
    n := args[0].Int()
    if n <= 1 {
        return n
    }
    a, b := 0, 1
    for i := 2; i <= n; i++ {
        a, b = b, a+b
    }
    return b
}

func main() {
    js.Global().Set("goFibonacci", js.FuncOf(fibonacci))
    // Block forever to keep the Go runtime alive
    select {}
}
Terminal
GOOS=js GOARCH=wasm go build -o main.wasm main.go
# Output: ~5MB (includes full Go runtime)

TinyGo (Small Binary)

Terminal
# Install TinyGo
brew install tinygo  # macOS
# or: https://tinygo.org/getting-started/install/

# Compile with TinyGo
tinygo build -o main.wasm -target wasm -opt=2 main.go
# Output: ~150KB
Go WASM Trade-offs

Standard Go produces the largest WASM binaries because it ships the full runtime. TinyGo reduces size dramatically but does not support every Go package. Check the TinyGo compatibility page before choosing it for a project that uses complex standard library packages like net/http or reflect.

Load Go WASM in the Browser

index.html
<!-- Copy wasm_exec.js from your Go installation -->
<script src="wasm_exec.js"></script>
<script>
  const go = new Go();

  WebAssembly.instantiateStreaming(
    fetch('main.wasm'),
    go.importObject
  ).then(result => {
    go.run(result.instance);
    // Now goFibonacci is available globally
    console.log(goFibonacci(40)); // 102334155
  });
</script>

JavaScript and WASM Interop

The boundary between JavaScript and WASM is where most complexity lives. WASM can only work with numbers (i32, i64, f32, f64). Strings, objects, arrays, and DOM nodes do not exist in WASM. Every non-numeric value must be serialized into linear memory as bytes.

Passing Strings

JavaScript
// Encode a string into WASM memory
function passString(instance, str) {
  const encoder = new TextEncoder();
  const bytes = encoder.encode(str);

  // Allocate memory in the WASM module
  const ptr = instance.exports.alloc(bytes.length + 1);

  // Write bytes into WASM memory
  const memory = new Uint8Array(instance.exports.memory.buffer);
  memory.set(bytes, ptr);
  memory[ptr + bytes.length] = 0; // null terminator

  return { ptr, len: bytes.length };
}

// Read a string from WASM memory
function readString(instance, ptr, len) {
  const memory = new Uint8Array(instance.exports.memory.buffer);
  const bytes = memory.slice(ptr, ptr + len);
  return new TextDecoder().decode(bytes);
}

Toolchains like wasm-bindgen (Rust) and Emscripten (C/C++) generate this glue code automatically. If you are building with raw WASM, you write it yourself. If you are using a high-level toolchain, you rarely touch it directly.

Sharing Typed Arrays

The most efficient way to exchange large data is through shared views of WASM linear memory. No copying required.

JavaScript
// Write an array of floats into WASM memory
const data = new Float64Array([1.5, 2.7, 3.14, 4.0, 5.5]);

// Get a pointer to free memory in the WASM module
const ptr = instance.exports.alloc(data.length * 8); // 8 bytes per f64

// Create a view into WASM memory and copy data in
const wasmMemory = new Float64Array(instance.exports.memory.buffer);
wasmMemory.set(data, ptr / 8); // divide by 8 for Float64 offset

// Call WASM function that processes the array in place
const sum = instance.exports.sum_array(ptr, data.length);

// Free the memory when done
instance.exports.dealloc(ptr, data.length * 8);

For validating your JavaScript interop code, the JavaScript Formatter cleans up messy glue code with consistent indentation.

Real-World Use Cases

WebAssembly is not theoretical. Production applications use it today for workloads that JavaScript handles poorly.

Image and Video Processing

Figma uses a C++ rendering engine compiled to WASM to power its design tool. Every shape, gradient, and blend mode runs in WASM with performance close to a native desktop app. Google Meet uses WASM for real-time background blur and replacement, processing each video frame in under 16ms. Photon is a Rust-to-WASM image processing library that applies filters 3-10x faster than Canvas API equivalents.

Games and 3D

Unity and Unreal Engine both export to WebAssembly for browser-based games. Doom 3 has been ported to run entirely in the browser via Emscripten. The consistent performance of WASM is critical for games because a single frame drop is visible at 60fps.

Cryptography

The Hash Generator demonstrates why WASM matters for crypto. Computing SHA-256 hashes in WASM is 2-5x faster than the JavaScript Web Crypto API for bulk hashing operations because WASM can use tight loops with known types. Libraries like argon2-browser compile the Argon2 password hashing algorithm from C to WASM.

Compression

Brotli and Zstandard decompression libraries are compiled to WASM for in-browser use. This is useful when you need to decompress data that the browser does not natively support, such as custom archive formats or game assets.

Data Processing

DuckDB-WASM runs a full analytical SQL database in the browser. It processes millions of rows of Parquet and CSV data without a server round-trip. Perspective, a streaming data visualization engine, uses WASM to aggregate and transform large datasets before rendering charts.

Performance: WASM vs JavaScript Benchmarks

Claims about WASM performance vary wildly. Here are measured results from realistic workloads, not microbenchmarks.

Where WASM Wins

Where JavaScript Wins or Ties

The practical rule: If your JavaScript code spends most of its time in a tight loop doing arithmetic on numbers or byte arrays, WASM will be faster. If it spends most of its time calling browser APIs or manipulating objects, WASM will not help.

WASI and Server-Side WASM

WebAssembly started in the browser, but its sandboxed execution model is equally valuable on servers. WASI (WebAssembly System Interface) is a standardized set of APIs that let WASM modules access operating system features, like file I/O, network sockets, clocks, and random number generation, in a controlled way.

Why WASI Matters

Running WASM Outside the Browser

hello.rs
fn main() {
    println!("Hello from WASI!");

    // File I/O works through WASI
    let contents = std::fs::read_to_string("input.txt")
        .unwrap_or_else(|_| "No file found".to_string());
    println!("File contents: {}", contents);
}
Terminal
# Compile to WASI target
rustup target add wasm32-wasip1
cargo build --target wasm32-wasip1 --release

# Run with Wasmtime (grant access to current directory)
wasmtime --dir . target/wasm32-wasip1/release/hello.wasm

WASM on the Edge

Cloudflare Workers, Fastly Compute, and Fermyon Spin all support WASM workloads. You write a request handler in Rust or Go, compile to WASM, and deploy to edge nodes worldwide. The cold start advantage is significant: Cloudflare Workers WASM functions typically start in under 1ms compared to 50-200ms for container-based functions.

Tools and Debugging

Browser DevTools

Chrome DevTools supports WASM debugging in the Sources panel. WASM modules appear under the wasm:// protocol. With debug symbols (compile with -g flag in Emscripten, or use a debug profile in Rust), you can:

Firefox Developer Tools offer similar capabilities with their own WASM inspector.

wasm-opt (Binaryen)

The wasm-opt tool from the Binaryen project applies additional optimizations to .wasm files after compilation. It can shrink binary size by 10-20% beyond what the compiler produces.

Terminal
# Install Binaryen
brew install binaryen  # macOS
apt install binaryen   # Ubuntu/Debian

# Optimize for size
wasm-opt -Oz -o output.wasm input.wasm

# Optimize for speed
wasm-opt -O3 -o output.wasm input.wasm

wasm2wat and wat2wasm

These tools convert between the binary WASM format and WAT (WebAssembly Text Format), a human-readable S-expression syntax. Useful for inspecting what your compiler actually produced.

Terminal
# Disassemble a .wasm file to readable text
wasm2wat module.wasm -o module.wat

# Assemble text format back to binary
wat2wasm module.wat -o module.wasm
module.wat (example output)
(module
  (func $fibonacci (param $n i32) (result i32)
    (local $a i32)
    (local $b i32)
    (local $i i32)
    (local $temp i32)
    ;; if n <= 1, return n
    (if (i32.le_s (local.get $n) (i32.const 1))
      (then (return (local.get $n)))
    )
    (local.set $b (i32.const 1))
    (local.set $i (i32.const 2))
    (block $break
      (loop $continue
        (local.set $temp (i32.add (local.get $a) (local.get $b)))
        (local.set $a (local.get $b))
        (local.set $b (local.get $temp))
        (local.set $i (i32.add (local.get $i) (i32.const 1)))
        (br_if $continue (i32.le_s (local.get $i) (local.get $n)))
      )
    )
    (local.get $b)
  )
  (export "fibonacci" (func $fibonacci))
)

Inspect the Base64 Encoder/Decoder if you need to embed small WASM modules as base64 strings in your JavaScript bundles, avoiding an extra network request for tiny modules.

Twiggy (Code Size Profiler)

Twiggy analyzes .wasm binaries and shows which functions contribute the most to file size. It helps you identify bloated dependencies and dead code that survived tree-shaking.

Terminal
cargo install twiggy
twiggy top module.wasm
twiggy dominators module.wasm

Related Developer Tools

Free browser-based tools that complement WebAssembly development workflows.


Frequently Asked Questions

WebAssembly is faster than JavaScript for CPU-intensive tasks like image processing, physics simulations, cryptography, and video encoding. Benchmarks consistently show WASM running 1.5x to 20x faster than equivalent JavaScript for compute-heavy workloads. However, JavaScript can be faster for DOM manipulation and short-lived tasks because calling between JavaScript and WASM has overhead. The practical advice is to use WASM for the hot loop where you spend most CPU cycles and keep DOM interaction in JavaScript.

Rust is the most popular choice for WebAssembly in 2026. It produces the smallest binaries, has first-class WASM support through wasm-pack and wasm-bindgen, and its ownership model eliminates the need for a garbage collector in the output. C and C++ via Emscripten are good if you are porting existing native code. Go works with TinyGo for smaller binaries, though output sizes are still larger than Rust. Other options include AssemblyScript (TypeScript-like syntax), Zig, and Kotlin. For new projects targeting the browser, Rust gives the best combination of binary size, performance, and tooling.

WebAssembly cannot access the DOM directly. WASM modules operate on linear memory and can only call imported JavaScript functions. To manipulate the DOM, your WASM code calls a JavaScript function that performs the actual DOM operation. Libraries like wasm-bindgen for Rust and Emscripten for C/C++ generate the JavaScript glue code automatically, so you can write code that looks like direct DOM access even though it routes through JavaScript. The Component Model proposal aims to make this interop more efficient in future WASM versions.

WASI (WebAssembly System Interface) is a standardized API that lets WebAssembly modules access operating system features like file systems, network sockets, clocks, and random number generation. Standard WebAssembly only defines computation and memory; it has no built-in way to interact with the outside world beyond imported functions. WASI fills that gap for server-side and edge environments. With WASI, you can compile a Rust or C program to WASM and run it outside the browser using runtimes like Wasmtime, Wasmer, or WasmEdge. WASI Preview 2, stabilized in early 2024, introduced the Component Model for better language interoperability.

Chrome DevTools and Firefox Developer Tools both support WASM debugging. In Chrome, open the Sources panel, and WASM modules appear under the wasm:// protocol. If you compile with debug info (using -g in Emscripten or a debug profile in Rust), you can set breakpoints in the original source code rather than the WASM binary. Chrome supports DWARF debug info for C/C++ and Rust source-level debugging. You can also inspect the linear memory as a hex dump, view the WASM call stack, and use console.log from JavaScript interop functions. For profiling, the Chrome Performance tab shows WASM functions in flame charts with their original names when debug symbols are present.

NT

Christian Bucher

We build free developer tools including binary converters, hash generators, JavaScript formatters, and 269 more. All browser-based, no signup required.

269 Developer Tools, One Place

Browse 269 indexed tool pages with no QTool account required, and inspect the source on GitHub.

Open Source — Free Forever Try Free Tools

Related Articles

Built by Miguel

Need a custom tool or website?

From . Delivered in 24-48h. You own the code.

View Services →