WebAssembly gets talked about like it's magic. It's not. It's a specific tool with specific use cases. Here's the honest picture.
What WebAssembly Actually Is
WebAssembly (WASM) is a binary instruction format that runs in a sandboxed VM in the browser (and other environments). It's not a language — it's a compilation target.
You write code in C, C++, Rust, Go, or other languages. You compile it to WASM. The browser runs the WASM at near-native speed.
C / C++ / Rust / Go
↓ compile
WebAssembly (.wasm)
↓ load
Browser (V8/SpiderMonkey)
↓ run
Near-native performance
What "Near-Native Speed" Actually Means
WASM runs at roughly 70-90% of native speed for compute-intensive tasks. That's the marketing pitch.
The reality is more nuanced:
WASM is faster than JavaScript for:
- Tight numerical loops
- Heavy math operations
- Image/audio/video processing
- Cryptography
WASM is NOT faster than JavaScript for:
- DOM manipulation
- Calling JavaScript APIs
- Memory allocation patterns that don't match WASM's linear memory model
- Tasks where JS JIT has already optimized well
The bottleneck is often the bridge. Every time WASM calls a JavaScript function or vice versa, there's an overhead cost. For algorithms that frequently cross the JS/WASM boundary, this can eliminate the performance advantage.
Real-World Use Cases
FFmpeg.wasm — Video Processing
The canonical example. FFmpeg is 500,000+ lines of C, compiled to WASM.
import { FFmpeg } from "@ffmpeg/ffmpeg";
const ffmpeg = new FFmpeg();
await ffmpeg.load(); // ~30MB download, cached after first load
await ffmpeg.writeFile("input.mp4", await fetchFile(videoFile));
await ffmpeg.exec(["-i", "input.mp4", "-crf", "28", "output.mp4"]);
const result = await ffmpeg.readFile("output.mp4");
Performance: 3-5x slower than native FFmpeg on the same hardware. For a 100MB video, expect 2-5 minutes in browser vs 20-40 seconds server-side. Acceptable for privacy-sensitive use cases.
SQLite in the Browser
import initSqlJs from "sql.js";
const SQL = await initSqlJs({
locateFile: file => `https://cdn.jsdelivr.net/npm/sql.js@1.10.2/dist/${file}`
});
const db = new SQL.Database();
db.run("CREATE TABLE users (id INTEGER, name TEXT)");
db.run("INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob')");
const result = db.exec("SELECT * FROM users");
Useful for: local-first apps, offline data, processing SQLite database files users upload.
Image Codecs
Encoding AVIF, JPEG XL, and other modern formats requires codec libraries that aren't in browsers yet. WASM fills the gap.
// @jsquash/avif uses WASM to encode AVIF
import encode from "@jsquash/avif/encode";
const avifData = await encode(imageData, { quality: 70 });
PDF Parsing
Libraries like pdf.js use WASM for performance-critical parsing.
The SharedArrayBuffer Requirement
Multithreaded WASM (using Web Workers + shared memory) requires:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Without these headers, SharedArrayBuffer is unavailable, and many WASM libraries fall back to single-threaded mode (or break entirely).
Setting headers in Next.js:
// next.config.js
module.exports = {
async headers() {
return [{
source: "/(.*)",
headers: [
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
{ key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
],
}];
},
};
This breaks some third-party scripts and iframes. Audit your dependencies.
Loading WASM — Size and Caching
WASM files can be large. FFmpeg.wasm core: ~30MB. SQLite: ~1MB. Codec libraries: 1-5MB.
Strategies:
- Load lazily (only when user needs the feature)
- Show progress during load
- Cache aggressively (WASM files rarely change)
- Use CDN with long cache headers
// Show loading state
const [wasmLoaded, setWasmLoaded] = useState(false);
// Load only when needed
const handleProcess = async () => {
if (!wasmLoaded) {
await ffmpeg.load();
setWasmLoaded(true);
}
// ... process
};
When NOT to Use WebAssembly
When JavaScript is fast enough. JSON parsing, DOM manipulation, most business logic — JS JIT handles these well.
When the WASM/JS bridge overhead dominates. If your algorithm makes thousands of small calls between WASM and JS, the overhead compounds.
When bundle size matters more than performance. A 5MB WASM file for a 10ms operation is not worth it.
When a pure JS library exists and performs acceptably. pdf-lib (pure JS) is slower than a WASM-based PDF library but avoids the complexity.
The Rust + WASM Path
If you're writing a new WASM module (not using existing compiled libraries), Rust has the best WASM toolchain.
// 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-pack build --target web
import init, { fibonacci } from "./pkg/my_module.js";
await init();
console.log(fibonacci(40)); // Fast!
Summary
| Use Case | Use WASM? |
|---|---|
| Video processing | Yes |
| Audio processing | Yes |
| Image codec encoding | Yes |
| Heavy math/simulation | Yes |
| SQLite in browser | Yes |
| JSON parsing | No (JS is fine) |
| DOM manipulation | No |
| Simple calculations | No |
| Business logic | Probably not |
WASM is a powerful tool for a specific class of problems — existing native code that needs to run in the browser, or compute-intensive algorithms where JS JIT isn't enough. For most web application logic, JavaScript is the right choice.
FFmpeg.wasm powers the video and audio tools at ToolZip.
Top comments (0)