Sunday, 15 February 2026

WebAssembly (WASM): A Complete Beginner’s Tutorial

Standard

Introduction: What is WASM?

WebAssembly (WASM) is a low-level, binary instruction format designed to run fast, safely, and portably on the web and beyond. It lets you run code written in languages like Rust, C/C++, and Go inside the browser or on servers at near-native speed.

Think of WASM as a universal execution target: you compile your program to WASM once, and it runs consistently across platforms.

Figure 1: WebAssembly enables high-performance code execution in browsers and beyond. This diagram shows how WASM bridges the gap between native performance and web portability.

Key Characteristics

  • Binary format: Compact, efficient, and fast to parse
  • Stack-based virtual machine: Simple execution model
  • Sandboxed: Secure by design, no arbitrary system access
  • Portable: Runs the same way across different platforms
  • Fast: Near-native performance (typically 80-90% of native speed)

What WASM is NOT

  • Not a programming language: You write code in Rust, C++, Go, etc., then compile to WASM
  • Not a replacement for JavaScript: It works alongside JavaScript
  • Not just for the web: Can run on servers, edge computing, IoT devices

Why WASM Was Invented

JavaScript unlocked the web, but it has limits for:

  • CPU-heavy workloads: Image/video processing, physics simulations, cryptography
  • Large codebases: Games, CAD software, compilers
  • Predictable performance: JavaScript’s JIT compilation can be unpredictable
  • Memory control: Limited ability to manage memory efficiently

WASM solves this by:

  • Providing a compact binary format (faster load/parse than JavaScript)
  • Enabling near-native execution speed (predictable performance)
  • Running in a secure sandbox (better security model)
  • Working alongside JavaScript, not replacing it (seamless integration)

The Performance Gap

Before WASM, developers had to choose between:

  • JavaScript: Easy to use, but slower for compute-intensive tasks
  • Native plugins: Fast, but insecure and platform-specific
  • Server-side processing: Secure, but adds latency and server costs

WASM provides the best of all worlds: JavaScript’s ease of use, native performance, and web security.

Understanding the WASM Architecture

Figure 2: The WASM compilation pipeline. Source code in languages like Rust, C++, or Go is compiled to WASM binary format, which can then be executed in browsers or WASM runtimes.

Core Components

WASM Module

  • The compiled binary (.wasm file)
  • Contains functions, memory, tables, and imports/exports
  • Loaded once and can be instantiated multiple times

WASM Engine:

  •  Executes the module

    In browsers: V8 (Chrome), SpiderMonkey (Firefox), JavaScriptCore (Safari)
  • Standalone: Wasmtime, Wasmer, WAVM

Host Environment

  • JavaScript (or other host language)

    Loads and instantiates WASM modules
  • Provides imports (functions, memory) to WASM
  • Calls exported functions from WASM

Linear Memory

  • A contiguous array of bytes

    Shared between WASM and JavaScript
  • Accessed via typed arrays (Uint8Array, Int32Array, etc.)

Sandbox

  • Security boundary

    No direct file system access
  • No network access (unless provided by host)
  • No arbitrary system calls
  • How WASM Works: The Complete Flow

Figure 3: V8’s WASM compilation pipeline. This shows how WASM binaries are validated, decoded, compiled, and optimized before execution.


Figure 4: Detailed view of the WASM compilation and execution pipeline, showing the stages from binary loading to optimized execution.

Step-by-Step Execution Flow

1. Source Code: Write code in Rust, C++, Go, or another supported language
2. Compilation: Compiler (rustc, emcc, go compiler) generates .wasm binary

# Example: Rust to WASM
wasm-pack build --target web

3. Loading: JavaScript loads the WASM module

const wasmModule = await WebAssembly.instantiateStreaming(
  fetch('module.wasm')
);

4. Validation

  • WASM engine validates the binary
  • Checks instruction validity
  • Verifies type safety
  • Ensures memory safety

5. Compilation:

  • Engine compiles WASM to native code

    JIT (Just-In-Time): Compiles during execution
  • AOT (Ahead-Of-Time): Pre-compiles for faster startup
6. Execution
  • Native code runs at near-native speed

7. Interoperation

  • JavaScript and WASM call each other

    JavaScript calls WASM functions
  • WASM calls JavaScript functions (via imports)

  1. Getting Started: Your First WASM Project

Let’s create your first WASM project step by step. We’ll use Rust as it has excellent WASM tooling.

Prerequisites

  1. Install Rust: Visit rustup.rs
  2. Install wasm-packcargo install wasm-pack
  3. A modern browser: Chrome, Firefox, Safari, or Edge

Step 1: Create a New Rust Project

cargo new --lib wasm-hello
cd wasm-hello

Step 2: Configure Cargo.toml

Edit Cargo.toml:

[package]
name = "wasm-hello"
version = "0.1.0"
edition = "2021"

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

[dependencies]
wasm-bindgen = "0.2"

Step 3: Write Your First WASM Function

Edit src/lib.rs:

use wasm_bindgen::prelude::*;

// Import the `console.log` function from JavaScript
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

// Define a macro to make console.log easier to use
macro_rules! console_log {
    ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}

// Export a simple function
#[wasm_bindgen]
pub fn greet(name: &str) {
    console_log!("Hello, {}! Welcome to WebAssembly!", name);
}

// Export a function that returns a value
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

// Export a function that processes arrays
#[wasm_bindgen]
pub fn sum_array(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}

Step 4: Build the WASM Module

wasm-pack build --target web

This creates a pkg/ directory with:

  • wasm_hello_bg.wasm: The compiled WASM binary
  • wasm_hello.js: JavaScript bindings
  • wasm_hello.d.ts: TypeScript definitions

Step 5: Create an HTML File

Create index.html:

<!DOCTYPE html>
<html>
<head>
    <title>WASM Hello World</title>
</head>
<body>
    <h1>WebAssembly Demo</h1>
    <button id="greet-btn">Greet</button>
    <button id="add-btn">Add Numbers</button>
    <button id="sum-btn">Sum Array</button>
    <div id="output"></div>

    <script type="module">
        import init, { greet, add, sum_array } from './pkg/wasm_hello.js';
        
        async function run() {
            // Initialize the WASM module
            await init();
            
            const output = document.getElementById('output');
            
            // Greet button
            document.getElementById('greet-btn').addEventListener('click', () => {
                greet('WebAssembly Developer');
            });
            
            // Add button
            document.getElementById('add-btn').addEventListener('click', () => {
                const result = add(42, 17);
                output.textContent = `42 + 17 = ${result}`;
            });
            
            // Sum array button
            document.getElementById('sum-btn').addEventListener('click', () => {
                const numbers = [1, 2, 3, 4, 5, 10, 20];
                const result = sum_array(numbers);
                output.textContent = `Sum of [1,2,3,4,5,10,20] = ${result}`;
            });
        }
        
        run();
    </script>
</body>
</html>

Step 6: Serve and Test

You need a local server (WASM requires HTTP, not file://):

# Using Python
python3 -m http.server 8000

# Using Node.js (if you have http-server installed)
npx http-server

# Using Rust (if you have it installed)
cargo install basic-http-server
basic-http-server

Open http://localhost:8000 in your browser and click the buttons!

Languages You Can Use with WASM

Rust (Recommended for Beginners)

Pros:

  • Excellent tooling (wasm-packwasm-bindgen)
  • Memory safety without garbage collection
  • Great performance
  • Active community

Cons:

  • Steeper learning curve
  • Compile times can be slow

Best for: New projects, performance-critical code

C/C++

Pros:

  • Mature ecosystem (Emscripten toolchain)
  • Great for porting existing codebases
  • Maximum performance

Cons:

  • More complex setup
  • Manual memory management
  • Larger binaries

Best for: Porting existing C/C++ libraries

Go

Pros:

  • Simple syntax
  • Built-in WASM support
  • Easy to learn

Cons:

  • Larger runtime (includes garbage collector)
  • Slower than Rust/C++
  • Less control over memory

Best for: Simple projects, rapid prototyping

AssemblyScript

Pros:

  • TypeScript-like syntax
  • Familiar to web developers
  • Small binaries

Cons:

  • Less mature than Rust/C++
  • Limited ecosystem

Best for: Web developers familiar with TypeScript

Zig

Pros:

  • Modern systems language
  • Small binaries
  • Good performance

Cons:

  • Still emerging
  • Smaller community

Best for: Systems programming, experimental projects

WASM Language Comparison: Complexity vs Speed

LanguageSetup ComplexityRuntime SizePerformanceBest Use
RustMediumSmall⭐⭐⭐⭐⭐New high-perf code
C/C++HighMedium⭐⭐⭐⭐⭐Porting native libs
GoLow–MediumLarge⭐⭐⭐Simplicity, tooling
AssemblyScriptLowSmall⭐⭐⭐Web devs
ZigMediumSmall⭐⭐⭐⭐Systems work

WASM Memory Management

WASM uses a linear memory model: a single, contiguous array of bytes that can grow.

Understanding Linear Memory

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;

#[wasm_bindgen]
pub fn process_buffer(buffer: &[u8]) -> Vec<u8> {
    // Process the buffer (e.g., apply a filter)
    buffer.iter().map(|&x| x.wrapping_add(10)).collect()
}

#[wasm_bindgen]
pub fn allocate_buffer(size: usize) -> *mut u8 {
    let mut buffer = vec![0u8; size];
    let ptr = buffer.as_mut_ptr();
    std::mem::forget(buffer); // Prevent deallocation
    ptr
}

#[wasm_bindgen]
pub fn free_buffer(ptr: *mut u8, size: usize) {
    unsafe {
        let _ = Vec::from_raw_parts(ptr, size, size);
    }
}

Memory Sharing with JavaScript

// JavaScript side
const wasmModule = await WebAssembly.instantiateStreaming(
    fetch('module.wasm')
);

// Access WASM memory
const memory = wasmModule.instance.exports.memory;
const memoryView = new Uint8Array(memory.buffer);

// Write data to WASM memory
memoryView[0] = 42;
memoryView[1] = 24;

// Call WASM function that processes the memory
wasmModule.instance.exports.process_memory();

Best Practices for Memory

  1. Reuse buffers: Don’t allocate/deallocate frequently
  2. Use typed arrays: More efficient than regular arrays
  3. Monitor memory growth: Use memory.grow() carefully
  4. Free allocated memory: Prevent memory leaks

JavaScript Interoperability

WASM and JavaScript work together seamlessly. Here’s how:

Calling WASM Functions from JavaScript

// Rust/WASM
#[wasm_bindgen]
pub fn calculate_fibonacci(n: u32) -> u64 {
    if n <= 1 {
        return n as u64;
    }
    let mut a = 0u64;
    let mut b = 1u64;
    for _ in 2..=n {
        let temp = a + b;
        a = b;
        b = temp;
    }
    b
}
// JavaScript
import init, { calculate_fibonacci } from './pkg/module.js';

await init();
const result = calculate_fibonacci(40);
console.log(`Fibonacci(40) = ${result}`);

Calling JavaScript Functions from WASM

// Rust/WASM
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    // Call JavaScript's console.log
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
    
    // Call a custom JavaScript function
    #[wasm_bindgen(js_name = "customFunction")]
    fn custom_function(value: i32);
}

#[wasm_bindgen]
pub fn wasm_function() {
    log("Hello from WASM!");
    custom_function(42);
}
// JavaScript
function customFunction(value) {
    console.log(`Received from WASM: ${value}`);
}

// Make it available globally
window.customFunction = customFunction;

Passing Complex Data

// Rust - Using serde for JSON
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct Person {
    name: String,
    age: u32,
}

#[wasm_bindgen]
pub fn create_person(name: String, age: u32) -> JsValue {
    let person = Person { name, age };
    JsValue::from_serde(&person).unwrap()
}
// JavaScript
const person = create_person("Alice", 30);
console.log(person); // { name: "Alice", age: 30 }

Real-World Examples

Example 1: Image Processing

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn grayscale_image(pixels: &mut [u8]) {
    // Process every 4 bytes (RGBA)
    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;
        
        // Grayscale formula
        let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
        
        chunk[0] = gray; // R
        chunk[1] = gray; // G
        chunk[2] = gray; // B
        // chunk[3] stays as alpha
    }
}

Example 2: Mathematical Computation

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn matrix_multiply(
    a: &[f64],
    b: &[f64],
    n: usize
) -> Vec<f64> {
    let mut result = vec![0.0; n * n];
    
    for i in 0..n {
        for j in 0..n {
            for k in 0..n {
                result[i * n + j] += a[i * n + k] * b[k * n + j];
            }
        }
    }
    
    result
}

Example 3: String Processing

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn reverse_string(s: &str) -> String {
    s.chars().rev().collect()
}

#[wasm_bindgen]
pub fn count_words(text: &str) -> usize {
    text.split_whitespace().count()
}

Browser Support and Deployment

Browser Support

All modern browsers support WASM:

  • Google Chrome: v57+ (March 2017)
  • Mozilla Firefox: v52+ (March 2017)
  • Apple Safari: v11+ (September 2017)
  • Microsoft Edge: v16+ (October 2017)

Support includes:

  • Core WASM 1.0
  • Streaming compilation
  • Threads (experimental)
  • SIMD (experimental)
  • Reference types
  • Tail calls

Deployment Best Practices

1. Compress WASM files: Use gzip or brotli compression

# Server should compress .wasm files
# Nginx example:
location ~* \.wasm$ {
    gzip on;
    gzip_types application/wasm;
}

2. Use streaming compilation: Faster startup

// Good: Streaming
const module = await WebAssembly.instantiateStreaming(
    fetch('module.wasm')
);

// Avoid: Non-streaming
const bytes = await fetch('module.wasm').then(r => r.arrayBuffer());
const module = await WebAssembly.instantiate(bytes);

3. Lazy load: Only load WASM when needed

async function loadWasmWhenNeeded() {
    if (!wasmModule) {
        wasmModule = await import('./pkg/module.js');
        await wasmModule.default();
    }
    return wasmModule;
}

4. Cache WASM modules: Use service workers or HTTP caching

Performance Considerations

When WASM Outperforms JavaScript

  • CPU-intensive tasks: Image processing, cryptography, physics
  • Large loops: Mathematical computations
  • Memory-intensive operations: Large array manipulations
  • Predictable workloads: Consistent performance matters

When JavaScript is Fine

  • DOM manipulation: JavaScript is optimized for this
  • Small scripts: Overhead of WASM not worth it
  • Rapid prototyping: JavaScript is faster to write
  • Simple logic: No performance benefit

Performance Tips

1. Minimize JS ↔ WASM calls: Batch operations

// Bad: Many small calls
for item in items {
    process_item(item); // Called from JS
}

// Good: One call with batch
process_batch(items); // Single call
2. Use typed arrays: Faster than regular arrays
3. Avoid unnecessary allocations: Reuse buffers
4. Profile your code: Use browser dev tools

Common Use Cases

1. Image and Video Processing

  • Figma: Real-time graphics rendering
  • FFmpeg.wasm: Video/audio processing in browser
  • Image filters: Instagram-like effects

2. Games and Simulations

  • Unity WebGL: Game engines
  • Physics engines: Real-time simulations
  • 3D graphics: WebGL acceleration

3. Data Processing

  • SQLite WASM: Embedded database
  • CSV parsing: Large file processing
  • Data compression: Client-side compression

4. Cryptography

  • Encryption/Decryption: Client-side security
  • Hashing: Password hashing
  • Digital signatures: Cryptographic operations

5. Scientific Computing

  • Numerical analysis: Complex calculations
  • Machine learning inference: Running ML models
  • Simulations: Scientific modeling

6. Compilers and Interpreters

  • Language runtimes: Python, Lua in browser
  • Code transpilation: Source-to-source compilation
  • Virtual machines: Custom VMs

Limitations and When Not to Use WASM

Limitations

1. No direct DOM access: Must go through JavaScript

// This doesn't exist in pure WASM
// document.getElementById("myDiv") // ❌

// You need JavaScript bridge
#[wasm_bindgen]
extern "C" {
    fn get_element_by_id(id: &str) -> JsValue;
}
2. Debugging challenges: Harder than JavaScript debugging
3. Binary size: Can be larger than JS for small tasks
4. Startup overhead: Module loading and compilation time
5. Limited garbage collection: Manual memory management in some languages

When NOT to Use WASM

  • Simple UI logic: JavaScript is better
  • Small scripts: Overhead not worth it
  • Rapid prototyping: JavaScript is faster to develop
  • DOM-heavy applications: JavaScript is optimized for this
  • Simple calculations: No performance benefit

When to Use WASM

✅ DO use WASM for:

  • CPU-intensive computations
  • Porting existing C/C++/Rust codebases
  • Performance-critical code
  • Large codebases that benefit from compilation
  • Cross-platform consistency

❌ DON’T use WASM for:

  • Simple DOM manipulation
  • Small utility functions
  • Rapid prototyping
  • Code that’s already fast enough in JavaScript

Best Practices

1. Start Small

Begin with simple functions and gradually add complexity.

2. Profile First

Don’t assume WASM is faster. Measure:

console.time('js-version');
// JavaScript code
console.timeEnd('js-version');

console.time('wasm-version');
// WASM code
console.timeEnd('wasm-version');

3. Error Handling

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn safe_divide(a: f64, b: f64) -> Result<f64, JsValue> {
    if b == 0.0 {
        Err(JsValue::from_str("Division by zero"))
    } else {
        Ok(a / b)
    }
}

4. Type Safety

Use TypeScript definitions generated by wasm-pack:

// Generated .d.ts file
export function add(a: number, b: number): number;

5. Testing

Test WASM modules thoroughly:

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }
}

6. Documentation

Document your WASM functions:

/// Adds two numbers together.
/// 
/// # Arguments
/// 
/// * `a` - First number
/// * `b` - Second number
/// 
/// # Returns
/// 
/// The sum of `a` and `b`
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Conclusion

WebAssembly is a powerful technology that extends what’s possible on the web. It doesn’t replace JavaScript, it complements it by enabling high-performance code execution where needed.

Key Takeaways

  1. WASM is for performance: Use it when JavaScript isn’t fast enough
  2. Works alongside JavaScript: They complement each other
  3. Multiple language support: Choose based on your needs
  4. Secure and portable: Runs safely across platforms
  5. Growing ecosystem: More tools and libraries every day

Next Steps

  • Build your first WASM project
  • Explore different languages (Rust, Go, C++)
  • Profile and optimize your code
  • Deploy to production
  • Contribute to the WASM ecosystem

Bibilography

Remember & Please Note: WASM is a tool, not a silver bullet. Use it where it makes sense, and JavaScript where it doesn’t. The best applications use both together!