WebAssembly Guide: Running High-Performance Code in the Browser

Master the fundamentals of WebAssembly and learn to write high-performance web applications using Rust, C++, and more

WebAssembly Guide: Running High-Performance Code in the Browser

WebAssembly (Wasm) is redefining the boundaries of web development. It enables running code written in C++, Rust, Go, and other languages in the browser with near-native performance. This article will give you a comprehensive understanding of WebAssembly.

What is WebAssembly?

Core Concepts

WebAssembly's Position:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   Traditional Web             With WebAssembly      │
│   ┌─────────────┐            ┌─────────────┐       │
│   │ JavaScript  │            │ JavaScript  │       │
│   │ (interpreted)│            │   + Wasm    │       │
│   └─────────────┘            └─────────────┘       │
│         ↓                          ↓               │
│   Limited by JS engine       Near-native perf      │
│   Complex compute difficult  Run any language      │
│   JavaScript only            Reuse existing libs   │
│                                                     │
└─────────────────────────────────────────────────────┘

What WebAssembly is NOT

Common Misconceptions Clarified:
┌─────────────────────────────────────────────────────┐
│ ❌ Wasm will replace JavaScript                     │
│ ✅ Wasm complements JS, each has its strengths     │
│                                                     │
│ ❌ Wasm is a programming language                   │
│ ✅ Wasm is a compilation target from other langs   │
│                                                     │
│ ❌ Wasm only runs in browsers                       │
│ ✅ Wasm can run server-side too (WASI)             │
│                                                     │
│ ❌ Wasm can directly manipulate DOM                 │
│ ✅ Wasm needs JavaScript bridge for DOM access     │
└─────────────────────────────────────────────────────┘

Use Cases

ScenarioExamplesWhy Wasm
Image/Video ProcessingFigma, PhotopeaCompute-intensive
Game EnginesUnity WebGLNeed high frame rates
Audio ProcessingSpotify decoderReal-time processing
Crypto/CompressionPassword managersSecurity + Performance
CAD/3D ModelingAutoCAD WebComplex geometry
Scientific ComputingData visualizationHeavy math operations

Getting Started

Option 1: Using AssemblyScript (TypeScript-like)

AssemblyScript is the easiest way to start—syntax is almost identical to TypeScript.

# Initialize project
npm init -y
npm install --save-dev assemblyscript
npx asinit .
// assembly/index.ts
export function fibonacci(n: i32): i32 {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

export function factorial(n: i32): i32 {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}
# Compile
npm run asbuild
// Use in JavaScript
async function loadWasm() {
  const { fibonacci, factorial } = await import('./build/release.js');

  console.log(fibonacci(40));  // Fast calculation
  console.log(factorial(10));
}

Option 2: Using Rust + wasm-pack

Rust has the most mature WebAssembly ecosystem.

# Install tools
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install wasm-pack

# Create project
cargo new --lib wasm-demo
cd wasm-demo
# Cargo.toml
[package]
name = "wasm-demo"
version = "0.1.0"
edition = "2021"

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

[dependencies]
wasm-bindgen = "0.2"
// src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2)
    }
}

// Expose structs
#[wasm_bindgen]
pub struct Counter {
    count: i32,
}

#[wasm_bindgen]
impl Counter {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Counter {
        Counter { count: 0 }
    }

    pub fn increment(&mut self) {
        self.count += 1;
    }

    pub fn get_count(&self) -> i32 {
        self.count
    }
}
# Compile
wasm-pack build --target web
// Use in JavaScript
import init, { greet, fibonacci, Counter } from './pkg/wasm_demo.js';

async function main() {
  await init();

  console.log(greet('World'));      // "Hello, World!"
  console.log(fibonacci(10));       // 55

  const counter = new Counter();
  counter.increment();
  counter.increment();
  console.log(counter.get_count()); // 2
}

main();

Understanding Wasm Deeply

Module Structure

Wasm Binary Structure:
┌─────────────────────────────────────────────────────┐
│ Magic Number (0x00 0x61 0x73 0x6D) = "\0asm"       │
│ Version (0x01 0x00 0x00 0x00) = 1                  │
├─────────────────────────────────────────────────────┤
│ Type Section     - Function signature definitions   │
│ Import Section   - Imported functions/memory/tables │
│ Function Section - Function declarations            │
│ Table Section    - Function pointer tables          │
│ Memory Section   - Linear memory definitions        │
│ Global Section   - Global variables                 │
│ Export Section   - Exported functions/memory        │
│ Start Section    - Startup function                 │
│ Element Section  - Table initialization data        │
│ Code Section     - Function bodies (bytecode)       │
│ Data Section     - Memory initialization data       │
└─────────────────────────────────────────────────────┘

Type System

Wasm Native Types:
┌─────────────────────────────────────────────────────┐
│ Numeric Types                                       │
│ ├─ i32  - 32-bit integer                           │
│ ├─ i64  - 64-bit integer                           │
│ ├─ f32  - 32-bit float                             │
│ └─ f64  - 64-bit float                             │
│                                                     │
│ Reference Types (Wasm 2.0)                          │
│ ├─ funcref  - Function reference                   │
│ └─ externref - External reference (JS objects)     │
│                                                     │
│ Vector Types (SIMD)                                 │
│ └─ v128 - 128-bit vector                           │
└─────────────────────────────────────────────────────┘

Memory Model

// Wasm uses linear memory (one big ArrayBuffer)
const memory = new WebAssembly.Memory({
  initial: 1,    // Initial 1 page (64KB)
  maximum: 10    // Maximum 10 pages
});

// Can directly manipulate memory
const buffer = new Uint8Array(memory.buffer);
buffer[0] = 42;

// Accessing memory in Rust
#[wasm_bindgen]
pub fn sum_array(ptr: *const i32, len: usize) -> i32 {
    let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
    slice.iter().sum()
}

JavaScript and Wasm Interaction

Basic Loading Methods

// Method 1: fetch + instantiate
async function loadWasm() {
  const response = await fetch('module.wasm');
  const bytes = await response.arrayBuffer();
  const { instance } = await WebAssembly.instantiate(bytes, imports);
  return instance.exports;
}

// Method 2: instantiateStreaming (recommended, faster)
async function loadWasmStreaming() {
  const { instance } = await WebAssembly.instantiateStreaming(
    fetch('module.wasm'),
    imports
  );
  return instance.exports;
}

// Method 3: Compile and cache
async function loadWasmWithCache() {
  const cache = await caches.open('wasm-cache');
  let module = await cache.match('module.wasm');

  if (!module) {
    const response = await fetch('module.wasm');
    await cache.put('module.wasm', response.clone());
    module = response;
  }

  const bytes = await module.arrayBuffer();
  return WebAssembly.instantiate(bytes, imports);
}

Passing Complex Data

// Rust side
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};

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

#[wasm_bindgen]
pub fn process_user(val: JsValue) -> JsValue {
    let user: User = serde_wasm_bindgen::from_value(val).unwrap();
    let result = User {
        name: user.name.to_uppercase(),
        age: user.age + 1,
    };
    serde_wasm_bindgen::to_value(&result).unwrap()
}
// JavaScript side
import { process_user } from './pkg/module.js';

const user = { name: 'Alice', age: 25 };
const result = process_user(user);
console.log(result);  // { name: 'ALICE', age: 26 }

Calling JavaScript Functions

// Rust side importing JS functions
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    // Import console.log
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);

    // Import custom function
    fn alert(s: &str);

    // Import function with return value
    fn get_current_time() -> f64;
}

#[wasm_bindgen]
pub fn greet() {
    log("Hello from Rust!");
    let time = get_current_time();
    log(&format!("Current time: {}", time));
}
// JavaScript side providing functions
const imports = {
  env: {
    alert: (ptr, len) => window.alert(readString(ptr, len)),
    get_current_time: () => Date.now()
  }
};

Performance Optimization

Compilation Optimization

# Cargo.toml - Release configuration
[profile.release]
opt-level = 3        # Highest optimization level
lto = true           # Link-time optimization
codegen-units = 1    # Single codegen unit (better optimization)
panic = "abort"      # Abort on panic (reduces size)
# Further optimize with wasm-opt
wasm-opt -O3 -o optimized.wasm input.wasm

# Minimize size
wasm-opt -Oz -o small.wasm input.wasm

Reducing Size

// Avoid unnecessary dependencies
// Using no_std can significantly reduce size
#![no_std]

// Custom panic handler
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}
# Size comparison
Original compile: ~100KB
opt-level=3: ~80KB
wasm-opt -O3: ~60KB
wasm-opt -Oz: ~40KB
After gzip: ~15KB

SIMD Acceleration

// Use SIMD instructions for vector operations
use std::arch::wasm32::*;

#[wasm_bindgen]
pub fn dot_product_simd(a: &[f32], b: &[f32]) -> f32 {
    let mut sum = f32x4_splat(0.0);

    for i in (0..a.len()).step_by(4) {
        let va = f32x4(a[i], a[i+1], a[i+2], a[i+3]);
        let vb = f32x4(b[i], b[i+1], b[i+2], b[i+3]);
        sum = f32x4_add(sum, f32x4_mul(va, vb));
    }

    f32x4_extract_lane::<0>(sum) +
    f32x4_extract_lane::<1>(sum) +
    f32x4_extract_lane::<2>(sum) +
    f32x4_extract_lane::<3>(sum)
}

Multi-threading

// Using Web Workers + SharedArrayBuffer
use wasm_bindgen::prelude::*;
use rayon::prelude::*;

#[wasm_bindgen]
pub fn parallel_sum(data: &[i32]) -> i32 {
    data.par_iter().sum()
}
// JavaScript side enabling multi-threading
// Requires correct HTTP headers
// Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corp

Practical Examples

Example 1: Image Processing

use wasm_bindgen::prelude::*;
use image::{ImageBuffer, Rgba};

#[wasm_bindgen]
pub fn apply_grayscale(data: &mut [u8], width: u32, height: u32) {
    for y in 0..height {
        for x in 0..width {
            let idx = ((y * width + x) * 4) as usize;
            let r = data[idx] as f32;
            let g = data[idx + 1] as f32;
            let b = data[idx + 2] as f32;

            // Grayscale conversion formula
            let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;

            data[idx] = gray;
            data[idx + 1] = gray;
            data[idx + 2] = gray;
        }
    }
}

#[wasm_bindgen]
pub fn apply_blur(data: &mut [u8], width: u32, height: u32, radius: u32) {
    // Gaussian blur implementation
    let kernel_size = radius * 2 + 1;
    // ... convolution operations
}
// Using in Canvas
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

// Call Wasm processing
apply_grayscale(imageData.data, canvas.width, canvas.height);

// Display result
ctx.putImageData(imageData, 0, 0);

Example 2: Markdown Parser

use wasm_bindgen::prelude::*;
use pulldown_cmark::{Parser, Options, html};

#[wasm_bindgen]
pub fn parse_markdown(input: &str) -> String {
    let mut options = Options::empty();
    options.insert(Options::ENABLE_STRIKETHROUGH);
    options.insert(Options::ENABLE_TABLES);

    let parser = Parser::new_ext(input, options);
    let mut html_output = String::new();
    html::push_html(&mut html_output, parser);

    html_output
}

Example 3: Encryption Tools

use wasm_bindgen::prelude::*;
use sha2::{Sha256, Digest};
use aes_gcm::{Aes256Gcm, Key, Nonce};
use aes_gcm::aead::{Aead, NewAead};

#[wasm_bindgen]
pub fn hash_sha256(data: &[u8]) -> Vec<u8> {
    let mut hasher = Sha256::new();
    hasher.update(data);
    hasher.finalize().to_vec()
}

#[wasm_bindgen]
pub fn encrypt_aes(key: &[u8], nonce: &[u8], plaintext: &[u8]) -> Vec<u8> {
    let key = Key::from_slice(key);
    let cipher = Aes256Gcm::new(key);
    let nonce = Nonce::from_slice(nonce);

    cipher.encrypt(nonce, plaintext).expect("encryption failed")
}

WASI: Server-Side WebAssembly

WebAssembly System Interface (WASI) enables Wasm to run on servers.

// Using WASI to access filesystem
use std::fs;
use std::io::Write;

fn main() {
    // Read file
    let content = fs::read_to_string("/input.txt").unwrap();

    // Process
    let result = content.to_uppercase();

    // Write file
    let mut file = fs::File::create("/output.txt").unwrap();
    file.write_all(result.as_bytes()).unwrap();
}
# Compile for WASI target
rustup target add wasm32-wasi
cargo build --target wasm32-wasi

# Run with Wasmtime
wasmtime run --dir=. target/wasm32-wasi/debug/app.wasm

Debugging Tips

Source Maps

# Generate source maps when compiling
wasm-pack build --dev

# Or generate manually
wasm-opt --debuginfo input.wasm -o output.wasm

Chrome DevTools

Steps to Debug Wasm:
1. Open DevTools → Sources
2. Find .wasm file
3. If source maps exist, you can see original code
4. Set breakpoints, step through
5. View Wasm stack frames and local variables

Performance Profiling

// Using Performance API
performance.mark('wasm-start');
const result = wasmFunction(data);
performance.mark('wasm-end');
performance.measure('wasm-execution', 'wasm-start', 'wasm-end');

const measures = performance.getEntriesByName('wasm-execution');
console.log(`Execution time: ${measures[0].duration}ms`);

Summary

WebAssembly brings new possibilities to the Web:

AspectJavaScriptWebAssembly
Use CasesUI, business logicCompute-intensive
PerformanceJIT optimizedNear-native
LanguagesJS/TS onlyMultiple
DOM AccessDirectNeeds bridge
Bundle SizeSmallerCan be larger
DebuggingEasyRequires tools

Key Takeaways:

  1. Wasm is a compilation target, not a programming language
  2. Complements JavaScript, doesn’t replace it
  3. Best for compute-intensive tasks
  4. Rust is currently the best language for Wasm development
  5. WASI takes Wasm beyond the browser

WebAssembly is making the Web platform more powerful, blurring the lines between browser apps and native applications.


WebAssembly: Bringing native power to the Web.