Skip to main content
Ganesh Joshi
Back to Blogs

WebAssembly for performance-critical web apps

February 16, 20265 min read
Tips
Binary or low-level code on screen, WebAssembly performance

WebAssembly (WASM) brings near-native performance to the browser. It's a compilation target for languages like Rust, C++, and Go, running in a sandboxed environment at speeds JavaScript can't match for compute-heavy tasks. About 5% of websites now use WebAssembly for everything from image processing to video games.

What WebAssembly is

WebAssembly is a binary instruction format designed for:

Property Benefit
Binary format Compact, fast to parse
Stack-based VM Efficient execution
Sandboxed Secure, can't access system
Language agnostic Compile from many languages
Deterministic Consistent behavior

It runs alongside JavaScript, not replacing it. JavaScript handles DOM and browser APIs; WASM handles computation.

When to use WebAssembly

Good use cases

Application Why WASM helps
Image processing Pixel-by-pixel manipulation
Video encoding/decoding ffmpeg.wasm
Cryptography Heavy number crunching
Physics simulations Real-time calculations
Games Game logic and rendering
Data compression zlib, brotli in browser
Scientific computing Numerical simulations
CAD/3D modeling Complex geometry

When JavaScript is fine

  • DOM manipulation
  • Event handling
  • Network requests
  • Simple data transformations
  • Most business logic

The overhead of calling WASM makes it inefficient for many small operations.

Performance comparison

Task JavaScript WebAssembly Speedup
Image blur (1000px) 150ms 20ms 7.5x
JSON parse (1MB) 10ms 50ms 0.2x (slower!)
SHA-256 hash 25ms 5ms 5x
Array sort (1M items) 100ms 80ms 1.25x

WASM excels at sustained computation. For small tasks or those involving serialization overhead, JavaScript may be faster.

Getting started with Rust

Rust has excellent WebAssembly support through wasm-pack.

Setup

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

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

# Install wasm-pack
cargo install wasm-pack

Create a project

cargo new --lib wasm-example
cd wasm-example

Configure Cargo.toml

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

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

[dependencies]
wasm-bindgen = "0.2"

Write Rust code

// src/lib.rs
use wasm_bindgen::prelude::*;

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

#[wasm_bindgen]
pub fn process_image(data: &mut [u8], width: u32, height: u32) {
    // Grayscale conversion
    for i in (0..data.len()).step_by(4) {
        let r = data[i] as f32;
        let g = data[i + 1] as f32;
        let b = data[i + 2] as f32;
        let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
        data[i] = gray;
        data[i + 1] = gray;
        data[i + 2] = gray;
    }
}

Build

wasm-pack build --target web

Output goes to pkg/:

  • wasm_example.js - JavaScript wrapper
  • wasm_example_bg.wasm - WebAssembly binary
  • wasm_example.d.ts - TypeScript definitions

Using in JavaScript

import init, { fibonacci, process_image } from './pkg/wasm_example.js';

async function main() {
  // Initialize WASM module
  await init();

  // Call exported functions
  const result = fibonacci(40);
  console.log('Fibonacci:', result);

  // Process image data
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

  process_image(imageData.data, canvas.width, canvas.height);

  ctx.putImageData(imageData, 0, 0);
}

main();

Memory management

WASM uses linear memory—a shared ArrayBuffer:

// Allocate memory in WASM
const ptr = wasm.allocate(size);

// Write data to WASM memory
const memory = new Uint8Array(wasm.memory.buffer);
memory.set(data, ptr);

// Call WASM function
wasm.process(ptr, data.length);

// Read results
const result = memory.slice(ptr, ptr + size);

// Free memory
wasm.deallocate(ptr);

For complex data, use wasm-bindgen's automatic handling.

Web Workers with WASM

Keep the main thread responsive:

// worker.ts
import init, { heavyComputation } from './pkg/wasm_example.js';

let initialized = false;

self.onmessage = async (e) => {
  if (!initialized) {
    await init();
    initialized = true;
  }

  const result = heavyComputation(e.data);
  self.postMessage(result);
};

// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url));

worker.postMessage(inputData);
worker.onmessage = (e) => {
  console.log('Result:', e.data);
};

Streaming compilation

Load large WASM files efficiently:

// Stream and compile in parallel with download
const wasmPromise = WebAssembly.instantiateStreaming(
  fetch('/module.wasm'),
  importObject
);

const { instance, module } = await wasmPromise;

This compiles while downloading—faster than downloading then compiling.

Debugging

Browser DevTools

Chrome and Firefox support WASM debugging:

  1. Open DevTools Sources panel
  2. Find .wasm file
  3. Set breakpoints in disassembly

Source maps

With Rust and wasm-pack:

wasm-pack build --dev  # Includes debug info

Bundle size optimization

Technique Savings
Release build 50-80%
wasm-opt 10-30%
gzip/brotli 60-70%
LTO 10-20%
# Optimized build
wasm-pack build --release

# Further optimization
wasm-opt -O3 -o optimized.wasm pkg/module_bg.wasm

Existing WASM libraries

Library Use case
ffmpeg.wasm Video processing
Squoosh Image compression
pdf.js PDF rendering
sql.js SQLite in browser
Pyodide Python in browser

Often you can use existing WASM libraries instead of writing your own.

Integration patterns

Function calls

Best for discrete computations:

const result = wasmModule.compute(input);

Shared memory

Best for large data processing:

const sharedBuffer = new SharedArrayBuffer(1024 * 1024);
const array = new Uint8Array(sharedBuffer);
// Both JS and WASM access same memory

Message passing

Best with Web Workers:

worker.postMessage({ type: 'process', data });

Summary

WebAssembly provides near-native performance for compute-heavy tasks in the browser. Use it for image processing, cryptography, games, and simulations. Rust with wasm-pack offers excellent tooling. Run WASM in Web Workers to keep the UI responsive. Use streaming compilation for large modules. For most web development, JavaScript remains sufficient—reach for WASM when profiling shows a clear bottleneck.

Frequently Asked Questions

WebAssembly (WASM) is a binary instruction format that runs in browsers at near-native speed. It's compiled from languages like Rust, C++, or Go and executes in a sandboxed environment alongside JavaScript.

Use WASM for CPU-intensive tasks: image/video processing, cryptography, physics simulations, games, and data processing. It's less useful for I/O-bound tasks or simple DOM manipulation.

For CPU-bound computation, WASM is typically 1.5-10x faster than JavaScript. The speedup depends on the workload. JavaScript is still efficient for DOM operations and many common tasks.

No, WASM can't access the DOM directly. It communicates with JavaScript through imported/exported functions and shared memory. JavaScript handles DOM operations based on WASM results.

Rust has excellent WASM tooling (wasm-pack, wasm-bindgen) and produces small binaries. C/C++ with Emscripten is mature. AssemblyScript offers TypeScript-like syntax. Choose based on team familiarity.

Related Posts