DEV Community

Cover image for WebAssembly is Ready (And You Should Use It)
Polliog
Polliog

Posted on

WebAssembly is Ready (And You Should Use It)

2015: "WebAssembly will revolutionize the web!"

2018: "Still not ready for production..."

2021: "Maybe for games?"

2025: WebAssembly is actually ready. And you're missing out.

I recently replaced a critical image processing function with WebAssembly (compiled from Rust). The result:

JavaScript: 850ms

WebAssembly: 18ms

That's 47x faster.

Here's when WASM makes sense, when it doesn't, and how to actually use it.


What is WebAssembly? (The 30-Second Version)

WebAssembly (WASM) is a binary instruction format that runs in the browser at near-native speed.

Think of it as:

  • A compilation target for languages like Rust, C++, Go, C#
  • A way to run performance-critical code in the browser
  • Not a replacement for JavaScript, but a complement

The key insight:

High-level language (Rust) → WASM → Browser → Near-native performance
Enter fullscreen mode Exit fullscreen mode

Instead of:

JavaScript → Browser → Interpreted/JIT compiled → Slower
Enter fullscreen mode Exit fullscreen mode

Why Now? What Changed?

WebAssembly has been "coming soon" for years. Here's what finally made it production-ready:

1. Browser Support is Universal (2023+)

All major browsers support WASM:

  • Chrome 57+ (2017)
  • Firefox 52+ (2017)
  • Safari 11+ (2017)
  • Edge 16+ (2017)

Current coverage: 97%+ of users

2. Tooling Matured

2018: Compiling Rust to WASM required custom build scripts

2025: wasm-pack build and you're done

3. Real Companies Use It in Production

Who's using WASM today:

  • Figma - Design tool (C++ → WASM for rendering)
  • Google Earth - 3D rendering
  • AutoCAD - Web version (C++ codebase → WASM)
  • Photoshop (web) - Image manipulation
  • Unity - WebGL games
  • Zoom - Video processing
  • Amazon Prime Video - Video codec

If it's good enough for Figma and Google, it's good enough for your app.


When You Should Use WebAssembly

✅ Use Case #1: CPU-Intensive Operations

Examples:

  • Image/video processing
  • Data compression
  • Cryptography
  • Scientific computing
  • Game physics engines

Real example from my project:

I had a JavaScript function that parsed and validated CSV files (100,000+ rows).

JavaScript implementation:

function validateCSV(csvString) {
  const rows = csvString.split('\n');
  const errors = [];

  for (let i = 0; i < rows.length; i++) {
    const cells = rows[i].split(',');
    // Complex validation logic
    if (!isValid(cells)) {
      errors.push({ row: i, error: 'Invalid format' });
    }
  }

  return errors;
}

// Time: 850ms for 100k rows
Enter fullscreen mode Exit fullscreen mode

Rust + WASM implementation:

#[wasm_bindgen]
pub fn validate_csv(csv_string: &str) -> JsValue {
    let mut errors = Vec::new();

    for (i, line) in csv_string.lines().enumerate() {
        let cells: Vec = line.split(',').collect();
        if !is_valid(&cells) {
            errors.push(ValidationError { row: i, message: "Invalid format" });
        }
    }

    serde_wasm_bindgen::to_value(&errors).unwrap()
}

// Time: 18ms for 100k rows
Enter fullscreen mode Exit fullscreen mode

Result: 47x faster

✅ Use Case #2: Porting Existing C/C++ Code

You have a C++ library and want to run it in the browser.

Examples:

  • Game engines (Unity, Unreal)
  • Scientific libraries (OpenCV, FFmpeg)
  • Compression (zlib, brotli)

Emscripten makes this trivial:

emcc mylib.c -o mylib.wasm
Enter fullscreen mode Exit fullscreen mode

✅ Use Case #3: Consistent Cross-Platform Performance

JavaScript performance varies wildly across devices:

  • High-end MacBook: Fast
  • Mid-range Android: Okay
  • Old iPhone: Slow

WASM performance is more consistent because it's closer to native code.

✅ Use Case #4: Security-Sensitive Code

WASM runs in a sandboxed environment with strict memory safety.

Use cases:

  • Cryptographic operations
  • DRM implementations
  • Code obfuscation (harder to reverse-engineer than JavaScript)

When You Should NOT Use WebAssembly

❌ Don't Use for DOM Manipulation

Why: WASM can't directly access the DOM.

Bad idea:

// This doesn't work!
#[wasm_bindgen]
pub fn update_ui() {
    document.getElementById("result").textContent = "Done";
}
Enter fullscreen mode Exit fullscreen mode

You have to:

// Return data to JS
#[wasm_bindgen]
pub fn process_data() -> String {
    "Done".to_string()
}
Enter fullscreen mode Exit fullscreen mode
// JS handles DOM
const result = wasm.process_data();
document.getElementById('result').textContent = result;
Enter fullscreen mode Exit fullscreen mode

If your code is mostly DOM manipulation, stick with JavaScript.

❌ Don't Use for Small Operations

WASM has overhead:

  • Loading the WASM module (~50-200ms)
  • Copying data between JS and WASM (memory boundary)

Example where WASM is slower:

// Simple math: JavaScript is fine
function add(a, b) {
  return a + b;
}
Enter fullscreen mode Exit fullscreen mode

Rule of thumb: If the operation takes < 10ms in JavaScript, WASM won't help.

❌ Don't Use If You Don't Know Rust/C++

WASM is not magic. You still need to write performant code in Rust/C++/Go.

Bad Rust code will be slow in WASM too.

If your team only knows JavaScript, the learning curve might not be worth it (unless performance is critical).


Real-World Performance Comparisons

I benchmarked several operations in JavaScript vs WASM:

Operation JavaScript WASM (Rust) Speedup
CSV parsing (100k rows) 850ms 18ms 47x
Image blur (2048×2048) 1,200ms 95ms 12x
JSON parsing (10MB) 450ms 280ms 1.6x
SHA-256 hash 12ms 1.2ms 10x
Fibonacci(40) 1,800ms 180ms 10x
DOM updates 5ms 15ms* 0.3x

*WASM is slower for DOM because of JS/WASM boundary crossing

Key insight: WASM shines for CPU-bound operations, not I/O or DOM.


Getting Started: Your First WASM Module

Step 1: Install Rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Enter fullscreen mode Exit fullscreen mode

Step 2: Install wasm-pack

cargo install wasm-pack
Enter fullscreen mode Exit fullscreen mode

Step 3: Create a Rust Project

cargo new --lib my-wasm-project
cd my-wasm-project
Enter fullscreen mode Exit fullscreen mode

Step 4: Update Cargo.toml

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

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

[dependencies]
wasm-bindgen = "0.2"
Enter fullscreen mode Exit fullscreen mode

Step 5: Write Rust Code

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

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

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Compile to WASM

wasm-pack build --target web
Enter fullscreen mode Exit fullscreen mode

Output:

pkg/
├── my_wasm_project_bg.wasm  (binary)
├── my_wasm_project.js       (glue code)
└── my_wasm_project.d.ts     (TypeScript types!)
Enter fullscreen mode Exit fullscreen mode

Step 7: Use in JavaScript





  WASM Demo



    import init, { greet, add } from './pkg/my_wasm_project.js';

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

      // Call Rust functions from JavaScript!
      console.log(greet('World'));  // "Hello, World!"
      console.log(add(5, 3));       // 8
    }

    run();



Enter fullscreen mode Exit fullscreen mode

That's it. You're running Rust in the browser.


Real Example: Image Processing

Let's build something useful: image brightness adjustment.

JavaScript Version (Baseline)

function adjustBrightness(imageData, factor) {
  const data = imageData.data;

  for (let i = 0; i < data.length; i += 4) {
    data[i] *= factor;     // Red
    data[i + 1] *= factor; // Green
    data[i + 2] *= factor; // Blue
    // data[i + 3] is alpha (unchanged)
  }

  return imageData;
}
Enter fullscreen mode Exit fullscreen mode

Performance: ~200ms for 1920×1080 image

Rust + WASM Version

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn adjust_brightness(data: &mut [u8], factor: f32) {
    for i in (0..data.len()).step_by(4) {
        data[i] = (data[i] as f32 * factor).min(255.0) as u8;
        data[i + 1] = (data[i + 1] as f32 * factor).min(255.0) as u8;
        data[i + 2] = (data[i + 2] as f32 * factor).min(255.0) as u8;
    }
}
Enter fullscreen mode Exit fullscreen mode

Performance: ~15ms for 1920×1080 image

13x faster!

Using It in JavaScript

import init, { adjust_brightness } from './pkg/image_processor.js';

async function processImage(imageElement, factor) {
  // Initialize WASM
  await init();

  // Get image data
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  canvas.width = imageElement.width;
  canvas.height = imageElement.height;
  ctx.drawImage(imageElement, 0, 0);

  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

  // Process with WASM
  adjust_brightness(imageData.data, factor);

  // Put back
  ctx.putImageData(imageData, 0, 0);
  return canvas.toDataURL();
}
Enter fullscreen mode Exit fullscreen mode

Passing Complex Data Between JS and WASM

The Problem

JavaScript and WASM have separate memory spaces. You can't just pass objects.

Solution 1: Use Simple Types

Best for: Numbers, strings

#[wasm_bindgen]
pub fn calculate(x: f64, y: f64) -> f64 {
    x * y + 42.0
}
Enter fullscreen mode Exit fullscreen mode

Solution 2: Use serde-wasm-bindgen

Best for: Complex objects, arrays

Rust side:

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

#[derive(Serialize, Deserialize)]
pub struct Point {
    pub x: f64,
    pub y: f64,
}

#[wasm_bindgen]
pub fn process_points(points: JsValue) -> JsValue {
    let points: Vec = serde_wasm_bindgen::from_value(points).unwrap();

    let transformed: Vec = points
        .iter()
        .map(|p| Point { x: p.x * 2.0, y: p.y * 2.0 })
        .collect();

    serde_wasm_bindgen::to_value(&transformed).unwrap()
}
Enter fullscreen mode Exit fullscreen mode

JavaScript side:

const points = [
  { x: 1, y: 2 },
  { x: 3, y: 4 }
];

const result = wasm.process_points(points);
console.log(result);  // [{ x: 2, y: 4 }, { x: 6, y: 8 }]
Enter fullscreen mode Exit fullscreen mode

Solution 3: Share Memory (Advanced)

Best for: Large buffers (images, audio)

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn get_buffer() -> Vec {
    vec![1, 2, 3, 4, 5]
}
Enter fullscreen mode Exit fullscreen mode
const buffer = wasm.get_buffer();
// buffer is a Uint8Array, zero-copy!
Enter fullscreen mode Exit fullscreen mode

Common Languages for WASM

1. Rust (★★★★★)

Pros:

  • Best tooling (wasm-pack, wasm-bindgen)
  • Memory safe (no garbage collection overhead)
  • Fast
  • Growing ecosystem

Cons:

  • Steep learning curve
  • Slower compile times

Best for: Production applications, performance-critical code

2. C/C++ (★★★★☆)

Pros:

  • Huge existing codebase to port
  • Emscripten is mature
  • Maximum control

Cons:

  • Manual memory management
  • Easy to introduce bugs
  • Larger WASM file sizes

Best for: Porting existing C++ projects (games, scientific libraries)

3. Go (★★★☆☆)

Pros:

  • Easy to learn
  • Good standard library
  • Garbage collected (easier memory management)

Cons:

  • Larger WASM files (includes GC)
  • Still experimental support
  • Not as fast as Rust/C++

Best for: If your team already knows Go

4. AssemblyScript (★★★☆☆)

Pros:

  • TypeScript-like syntax
  • Easy for JS developers
  • Good for simple use cases

Cons:

  • Limited ecosystem
  • Not as fast as Rust/C++
  • Smaller community

Best for: JavaScript developers dipping toes into WASM


Debugging WebAssembly

In the Browser

Chrome DevTools supports WASM debugging:

  1. Open DevTools → Sources
  2. Set breakpoints in WASM code
  3. Inspect memory

Chrome shows:

  • WASM source code (if source maps included)
  • Memory inspector
  • Call stack

With Logging

Rust side:

use wasm_bindgen::prelude::*;

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

#[wasm_bindgen]
pub fn debug_function(x: i32) -> i32 {
    log(&format!("Processing: {}", x));
    x * 2
}
Enter fullscreen mode Exit fullscreen mode

JavaScript side:

wasm.debug_function(42);  // Console: "Processing: 42"
Enter fullscreen mode Exit fullscreen mode

Performance Profiling

console.time('WASM');
wasm.expensive_operation();
console.timeEnd('WASM');
Enter fullscreen mode Exit fullscreen mode

Or use Chrome's Performance tab for detailed profiling.


WASM in Different Frameworks

React

import { useEffect, useState } from 'react';
import init, { process_data } from './pkg/my_wasm.js';

function App() {
  const [wasm, setWasm] = useState(null);

  useEffect(() => {
    init().then(() => {
      setWasm({ process_data });
    });
  }, []);

  const handleProcess = () => {
    if (wasm) {
      const result = wasm.process_data('input');
      console.log(result);
    }
  };

  return Process;
}
Enter fullscreen mode Exit fullscreen mode

Vue

<template>
  <button @click="process">Process</button>
</template>

<script>
import init, { process_data } from './pkg/my_wasm.js';

export default {
  data() {
    return { wasmReady: false };
  },
  async mounted() {
    await init();
    this.wasmReady = true;
  },
  methods: {
    process() {
      if (this.wasmReady) {
        const result = process_data('input');
        console.log(result);
      }
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Svelte

<script>
  import { onMount } from 'svelte';
  import init, { process_data } from './pkg/my_wasm.js';

  let wasmReady = false;

  onMount(async () => {
    await init();
    wasmReady = true;
  });

  function process() {
    if (wasmReady) {
      const result = process_data('input');
      console.log(result);
    }
  }
</script>

<button on:click={process} disabled={!wasmReady}>
  Process
</button>
Enter fullscreen mode Exit fullscreen mode

Production Considerations

1. File Size

WASM files can be large:

  • Simple Rust function: ~20KB
  • Complex app: 500KB - 2MB
  • C++ port: Can be 10MB+

Optimization tips:

# Optimize for size
wasm-pack build --target web --release

# Further optimization
wasm-opt -Oz -o output.wasm input.wasm
Enter fullscreen mode Exit fullscreen mode

Use gzip: WASM compresses well (50-70% size reduction).

2. Loading Strategy

Don't block the main thread:

// Bad: Blocks rendering
await init();
renderApp();

// Good: Load in parallel
Promise.all([
  init(),
  loadOtherAssets()
]).then(() => renderApp());
Enter fullscreen mode Exit fullscreen mode

3. Browser Support

Check for WASM support:

function supportsWasm() {
  try {
    if (typeof WebAssembly === "object" &&
        typeof WebAssembly.instantiate === "function") {
      const module = new WebAssembly.Module(
        Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)
      );
      return module instanceof WebAssembly.Module;
    }
  } catch (e) {}
  return false;
}

if (supportsWasm()) {
  // Use WASM
} else {
  // Fallback to JavaScript
}
Enter fullscreen mode Exit fullscreen mode

4. Caching

WASM files should be cached aggressively:

// Service Worker
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('wasm-cache-v1').then(cache => {
      return cache.addAll([
        '/pkg/my_wasm_bg.wasm',
        '/pkg/my_wasm.js'
      ]);
    })
  );
});
Enter fullscreen mode Exit fullscreen mode

Real-World Success Stories

Figma: 3x Faster Rendering

Before: Canvas rendering in JavaScript

After: C++ rendering engine compiled to WASM

Result:

  • 3x faster initial load
  • Smoother interactions
  • Support for massive files

Quote from Figma:

"WASM was the only way to achieve desktop-class performance in the browser."

AutoCAD: Entire App in Browser

Challenge: Run 35-year-old C++ codebase in browser

Solution: Compile to WASM using Emscripten

Result:

  • 1.5 million lines of C++ running in browser
  • Near-native performance
  • No plugins required

Google Earth: Smooth 3D

Before: Native plugin (Chrome only)

After: WASM + WebGL

Result:

  • Works in all browsers
  • Faster load times
  • Better security

Common Pitfalls

❌ Pitfall #1: Expecting Magic

WASM won't fix bad algorithms.

// This is still O(n²) and slow
pub fn bubble_sort(arr: &mut [i32]) {
    for i in 0..arr.len() {
        for j in 0..arr.len()-1 {
            if arr[j] > arr[j+1] {
                arr.swap(j, j+1);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Fix: Use good algorithms, then use WASM.

❌ Pitfall #2: Too Much JS ↔ WASM Communication

Bad:

for (let i = 0; i < 1000000; i++) {
  wasm.process_single_item(data[i]);  // 1M function calls!
}
Enter fullscreen mode Exit fullscreen mode

Good:

wasm.process_all_items(data);  // 1 function call
Enter fullscreen mode Exit fullscreen mode

Crossing the JS/WASM boundary is expensive. Batch operations.

❌ Pitfall #3: Not Measuring

Always benchmark:

// JavaScript version
console.time('JS');
processInJS(data);
console.timeEnd('JS');

// WASM version
console.time('WASM');
wasm.process(data);
console.timeEnd('WASM');
Enter fullscreen mode Exit fullscreen mode

WASM isn't always faster. Measure before committing.


Tools and Resources

Learning:

Tools:

Examples:


Should You Use WASM? (Decision Tree)

Does your app have performance problems?
├─ No → Don't use WASM (yet)
└─ Yes
   ├─ Is it CPU-bound? (parsing, computation, crypto)
   │  └─ Yes → Consider WASM ✅
   └─ Is it I/O-bound? (network, DOM updates)
      └─ No → WASM won't help ❌

Are you porting existing C/C++ code?
└─ Yes → WASM is perfect ✅

Do you need consistent cross-device performance?
└─ Yes → WASM helps ✅

Is your team willing to learn Rust/C++?
├─ Yes → Go for it ✅
└─ No → Maybe stick with JavaScript
Enter fullscreen mode Exit fullscreen mode

The Future of WebAssembly

Coming soon (2025-2026):

1. WASI (WebAssembly System Interface)

Run WASM outside the browser (Node.js, servers, IoT)

2. Garbage Collection Support

Better support for languages like Go, C#, Java

3. Threading Improvements

Better multi-threading in WASM

4. SIMD (Single Instruction, Multiple Data)

Even faster parallel operations


TL;DR - When to Use WASM

Use WebAssembly when:

  • ✅ You have CPU-intensive operations (parsing, crypto, image processing)
  • ✅ You're porting C/C++ code to the web
  • ✅ You need consistent performance across devices
  • ✅ JavaScript is measurably too slow

Don't use WebAssembly when:

  • ❌ Your code is mostly DOM manipulation
  • ❌ Operations take < 10ms in JavaScript
  • ❌ Your team doesn't know Rust/C++/Go
  • ❌ You haven't profiled to confirm JavaScript is the bottleneck

Getting started:

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

# Install wasm-pack
cargo install wasm-pack

# Create project
cargo new --lib my-wasm-project

# Build
wasm-pack build --target web
Enter fullscreen mode Exit fullscreen mode

Your Turn

Challenge: Find the slowest function in your app and port it to WASM.

Post your results:

  • What operation did you optimize?
  • JavaScript time vs WASM time
  • Was it worth it?

Drop your benchmarks in the comments! 👇


What's the first thing you want to try with WebAssembly? Image processing? Game physics? Crypto? Let me know and I'll help you get started!

P.S. - If you want a follow-up article on specific use cases (WASM for games, WASM for data science, WASM on the server), let me know!

Top comments (0)