Skip to content
Go back

WebAssembly Without the Hype

Updated:
Edit Views

Table of contents

Open Table of contents

Introduction

I skipped WebAssembly for years. Every article led with the hype (“near-native performance”) and ended with a Rust setup guide. No answer to the only question that matters: when does this actually help?

What changed: reading about how Figma built their browser editor. The vector renderer, the layout engine, the constraint solver are all C++ compiled to Wasm, not as a trick, but because JavaScript can’t run that kind of real-time math fast enough for a design tool. That’s a real problem, and Wasm solves it cleanly.

For CPU-heavy computation in the browser, it’s a different class of tool than JavaScript, not a replacement for it.

This post covers what Wasm actually is, how it fits alongside JavaScript, where it runs today, and a working Rust example you can build locally.

What WebAssembly actually is

WebAssembly is a binary instruction format, a compiled target rather than a language you write directly. You write code in Rust, C, C++, or Go, compile it to a .wasm file, and the browser runs that file at speeds close to native machine code.

All major browsers have supported Wasm since 2017, no plugins or flags required.

Compiler Rust/C/Go .wasm binary JavaScript DOM Browser APIs
How WebAssembly fits into the stack

JavaScript still owns the DOM. Wasm can’t touch it directly. The pattern is always the same: JavaScript loads the Wasm module, calls functions in it, gets results back, does something with those results. They work together. Wasm doesn’t replace the JS layer, it handles the computation that JS handles poorly.

Why JavaScript struggles with heavy computation

JavaScript is dynamically typed and runs through a JIT compiler that makes smart guesses about your code. For typical application code (fetching data, updating state, handling events) those guesses are good enough.

For tight computational loops (image processing, cryptography, physics simulations) the JIT’s assumptions break down, it deoptimizes, and performance falls off a cliff.

Wasm bypasses this. The binary is already compiled close to machine code, with no JIT warmup or type inference overhead. Performance stays consistent.

TaskJavaScriptWebAssembly
DOM manipulationNativeNot possible directly
Network requestsNativeNeeds JS bridge
UI logic / stateFineOverkill
Image processingSlowFast
CryptographySlowFast
3D rendering / physicsToo slowFast
Video encodingToo slowFast

Real uses in production

These are things it’s running right now:

The component model and WASI: Wasm outside the browser

Wasm isn’t browser-only.

WASI (WebAssembly System Interface) gives Wasm modules controlled access to the filesystem, network, clocks, and environment variables. With WASI, you can run .wasm files anywhere there’s a runtime: servers, edge nodes, IoT devices.

.wasm module Browser Node.js Wasmtime / Wasmer Cloudflare Workers Edge nodes / IoT Runs in JS sandbox Server-side processing Standalone runtime Edge compute Portable binary
Wasm's expanding runtime environment

Cloudflare Workers supports Wasm natively. You can write a Rust function, compile it to Wasm, and deploy it as an edge function that runs globally with sub-millisecond cold starts.

The Component Model (stabilized in 2025) is a spec that lets Wasm modules expose typed interfaces and compose with each other, regardless of what language they were written in. A Rust module and a Go module can call each other without going through JavaScript. This is early but it’s where server-side Wasm is heading.

A working example: Rust to Wasm

We’ll write a string processing function in Rust, compile it to Wasm, and call it from JavaScript.

How to start

You need Rust and wasm-pack:

# Install Rust
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

The Rust code

wasm-pack new wasm-hello
cd wasm-hello

Edit src/lib.rs:

use wasm_bindgen::prelude::*;

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

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

#[wasm_bindgen]
pub fn count_primes(n: u32) -> u32 {
    (2..=n)
        .filter(|&num| {
            let limit = (num as f64).sqrt() as u32;
            (2..=limit).all(|i| num % i != 0)
        })
        .count() as u32
}src/lib.rs

Build

wasm-pack build --target web

This generates a pkg/ folder with the .wasm binary, a JavaScript wrapper, and TypeScript types.

Use from JavaScript

<!DOCTYPE html>
<html>
<body>
  <script type="module">
    import init, { count_words, reverse_string, count_primes } from './pkg/wasm_hello.js';

    await init();

    const text = "WebAssembly runs at near-native speed in the browser";
    console.log(count_words(text));       // 9
    console.log(reverse_string("hello")); // "olleh"

    console.time("primes");
    console.log(count_primes(1_000_000)); // 78498
    console.timeEnd("primes");
  </script>
</body>
</html>index.html

await init() loads and compiles the .wasm binary asynchronously. After that, the exported functions work exactly like JavaScript functions. The caller has no idea they’re running Wasm.

What wasm-pack generated

pkg/
├── wasm_hello.js          ← JS wrapper with init() and exported functions
├── wasm_hello_bg.wasm     ← the actual binary
├── wasm_hello_bg.wasm.d.ts
└── wasm_hello.d.ts        ← TypeScript types for your exports

TypeScript types are generated automatically from your Rust signatures. You get type checking on the JavaScript side for free.

Using Wasm in a real project

If you’re on Vite, the integration is straightforward:

npm install vite-plugin-wasm
import { defineConfig } from "vite";
import wasm from "vite-plugin-wasm";

export default defineConfig({
  plugins: [wasm()],
});vite.config.ts

Then import directly:

import init, { process_pixels } from "./wasm/image_processor.js";

let wasmReady = false;

async function ensureWasm() {
  if (!wasmReady) {
    await init();
    wasmReady = true;
  }
}

export async function applyGrayscale(imageData: ImageData): Promise<ImageData> {
  await ensureWasm();

  const pixels = new Uint8Array(imageData.data.buffer);
  const processed = process_pixels(pixels, imageData.width, imageData.height);

  return new ImageData(
    new Uint8ClampedArray(processed),
    imageData.width,
    imageData.height
  );
}imageProcessor.ts

The pattern: initialize once, call as needed. Don’t re-initialize on every call. The .wasm binary loads once and stays in memory.

Limitations

Wasm has real tradeoffs.

  1. No direct DOM access. Wasm can’t read or write the DOM. Everything goes through JavaScript. If your bottleneck is “too many DOM updates,” Wasm won’t help. If it’s “too much computation between the DOM updates,” it will.
  2. Binary size. A compiled Rust binary can be 1–10MB before compression. Brotli/gzip compress it well, but the first load costs something. For a page that needs 2MB of Wasm, you want to lazy-load it: only initialize the module when the user reaches the feature that needs it.
  3. Garbage collection. The Wasm GC proposal (part of Wasm 3.0) adds garbage collection for languages like Java and Kotlin, but the Rust/C workflow means managing memory manually or through Rust’s ownership system. wasm-bindgen handles the JS/Wasm boundary automatically, but understanding what crosses that boundary still matters.
  4. Debugging is harder. Chrome DevTools supports Wasm debugging with source maps back to Rust, but it’s not as smooth as debugging JavaScript. Errors in Wasm show up as traps, not exceptions. Add instrumentation early.
  5. Thread model. Wasm threads require SharedArrayBuffer, which requires specific COOP/COEP security headers. Not all hosting environments support this without configuration. For parallel computation in Wasm, check whether your host supports the required headers before committing.

When to actually use Wasm

The practical checklist:

FAQ

Do I need to know Rust to use WebAssembly? For browser use cases, Rust with wasm-pack is the most practical path. The tooling is mature and the output is clean. You don't need to be a Rust expert; a working understanding of functions, types, and the basic ownership model is enough to write useful Wasm modules. If you're more comfortable with C/C++, Emscripten works well for porting existing libraries. AssemblyScript (TypeScript syntax compiled to Wasm) is the lowest barrier, but it produces less optimized output.
Can Wasm access the network or filesystem? In the browser: no, not directly. All network calls go through JavaScript's fetch. Outside the browser (with WASI), yes, with explicitly granted capabilities. That's one of WASI's design goals: a Wasm module only gets the permissions you hand it.
Is Wasm more secure than JavaScript? It runs in the same browser sandbox as JavaScript, so not more or less secure for the user. From a supply chain perspective, a compiled binary is harder to casually inspect than JavaScript source, which can be a downside. If you're consuming a third-party Wasm binary, you're trusting the compiled output more than you can trust JS.
Does Wasm work in all browsers? Yes. Chrome, Firefox, Safari, and Edge have all supported the MVP spec since 2017. Newer features like threads and SIMD have >95% support in 2026. The Component Model is still landing in runtimes but is production-ready in Wasmtime and Cloudflare Workers.
What about WebWorkers? Can Wasm run off the main thread? Yes, and this is the right pattern for heavy workloads. Run the Wasm module inside a Web Worker so the main thread stays unblocked. The Worker communicates with the main thread via postMessage. It's more setup than running on the main thread but it's how you avoid jank during computation.

Conclusion

WebAssembly sounds broader than it is. It’s not a JavaScript replacement or a full backend runtime (though WASI is slowly changing the latter). It runs compiled code in the browser at speeds JavaScript can’t match, for the specific class of problem where that gap matters.

If your application does any heavy client-side computation, Wasm is worth adding to your toolkit. The wasm-pack workflow is approachable even without deep Rust experience.

I went from “I’ll learn this eventually” to shipping it in production the same week I tried it. The toolchain is easier than it looks.

Start with the prime counter above. Get it building, call it from JavaScript, and go from there.


Share this post on:

Previous Post
Frontend Security - XSS, CSRF, Middleware Bypasses, and What Actually Gets You Hacked
Next Post
Husky and Conventional Commits — How to use Git Hooks Without the Pain