WebAssembly runs near-native speed in the browser. Here is how to use it for performance-critical tasks.
Write a Rust Module
// wasm/src/lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
if n <= 1 {
return n as u64;
}
let mut a: u64 = 0;
let mut b: u64 = 1;
for _ in 2..=n {
let temp = a + b;
a = b;
b = temp;
}
b
}
#[wasm_bindgen]
pub fn image_grayscale(pixels: &mut [u8]) {
for chunk in pixels.chunks_exact_mut(4) {
let gray = (0.299 * chunk[0] as f64
+ 0.587 * chunk[1] as f64
+ 0.114 * chunk[2] as f64) as u8;
chunk[0] = gray;
chunk[1] = gray;
chunk[2] = gray;
// chunk[3] (alpha) stays the same
}
}
#[wasm_bindgen]
pub struct Matrix {
rows: usize,
cols: usize,
data: Vec<f64>,
}
#[wasm_bindgen]
impl Matrix {
#[wasm_bindgen(constructor)]
pub fn new(rows: usize, cols: usize) -> Matrix {
Matrix {
rows,
cols,
data: vec![0.0; rows * cols],
}
}
pub fn set(&mut self, row: usize, col: usize, value: f64) {
self.data[row * self.cols + col] = value;
}
pub fn get(&self, row: usize, col: usize) -> f64 {
self.data[row * self.cols + col]
}
pub fn multiply(&self, other: &Matrix) -> Matrix {
assert_eq!(self.cols, other.rows);
let mut result = Matrix::new(self.rows, other.cols);
for i in 0..self.rows {
for j in 0..other.cols {
let mut sum = 0.0;
for k in 0..self.cols {
sum += self.get(i, k) * other.get(k, j);
}
result.set(i, j, sum);
}
}
result
}
}
Build With wasm-pack
# Install wasm-pack
cargo install wasm-pack
# Build the module
cd wasm
wasm-pack build --target web --out-dir ../public/wasm
Configure Next.js
// next.config.ts
import type { NextConfig } from "next";
const config: NextConfig = {
webpack(config) {
config.experiments = {
...config.experiments,
asyncWebAssembly: true,
};
return config;
},
};
export default config;
WASM Loader Hook
"use client";
import { useState, useEffect, useRef } from "react";
interface WasmModule {
fibonacci: (n: number) => bigint;
image_grayscale: (pixels: Uint8Array) => void;
Matrix: new (rows: number, cols: number) => {
set(row: number, col: number, value: number): void;
get(row: number, col: number): number;
multiply(other: unknown): unknown;
};
}
export function useWasm() {
const [module, setModule] = useState<WasmModule | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const loadedRef = useRef(false);
useEffect(() => {
if (loadedRef.current) return;
loadedRef.current = true;
async function loadWasm() {
try {
const wasm = await import("/public/wasm/wasm_bg.wasm");
const init = await import("/public/wasm/wasm.js");
await init.default();
setModule(init as unknown as WasmModule);
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
}
loadWasm();
}, []);
return { module, loading, error };
}
Alternative: Pure JS WASM Loading
"use client";
export async function loadWasmModule(url: string) {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(buffer, {
env: {
memory: new WebAssembly.Memory({ initial: 256 }),
},
});
return instance.exports;
}
Image Processing Example
"use client";
import { useWasm } from "@/hooks/use-wasm";
import { useRef, useState } from "react";
export function ImageProcessor() {
const { module, loading } = useWasm();
const canvasRef = useRef<HTMLCanvasElement>(null);
const [processing, setProcessing] = useState(false);
async function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file || !module || !canvasRef.current) return;
setProcessing(true);
const img = new Image();
img.onload = () => {
const canvas = canvasRef.current!;
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
// Process with WASM β much faster than JS for large images
const start = performance.now();
module.image_grayscale(imageData.data as unknown as Uint8Array);
const elapsed = performance.now() - start;
ctx.putImageData(imageData, 0, 0);
setProcessing(false);
console.log(`WASM processing took ${elapsed.toFixed(2)}ms`);
};
img.src = URL.createObjectURL(file);
}
return (
<div className="space-y-4">
<input
type="file"
accept="image/*"
onChange={handleFileSelect}
disabled={loading || processing}
className="text-sm"
/>
{processing && <p className="text-sm text-muted-foreground">Processing...</p>}
<canvas ref={canvasRef} className="max-w-full rounded-lg border" />
</div>
);
}
Performance Benchmark
"use client";
import { useWasm } from "@/hooks/use-wasm";
import { useState } from "react";
function jsFibonacci(n: number): number {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
const temp = a + b;
a = b;
b = temp;
}
return b;
}
export function WasmBenchmark() {
const { module, loading } = useWasm();
const [results, setResults] = useState<{ js: number; wasm: number } | null>(null);
function runBenchmark() {
if (!module) return;
const iterations = 10000;
const n = 40;
// JS benchmark
const jsStart = performance.now();
for (let i = 0; i < iterations; i++) jsFibonacci(n);
const jsTime = performance.now() - jsStart;
// WASM benchmark
const wasmStart = performance.now();
for (let i = 0; i < iterations; i++) module.fibonacci(n);
const wasmTime = performance.now() - wasmStart;
setResults({ js: jsTime, wasm: wasmTime });
}
return (
<div className="space-y-4">
<button
onClick={runBenchmark}
disabled={loading}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md text-sm"
>
Run Benchmark (fibonacci(40) x 10,000)
</button>
{results && (
<div className="text-sm space-y-1">
<p>JavaScript: {results.js.toFixed(2)}ms</p>
<p>WebAssembly: {results.wasm.toFixed(2)}ms</p>
<p className="font-medium">
WASM is {(results.js / results.wasm).toFixed(1)}x faster
</p>
</div>
)}
</div>
);
}
Need High-Performance Web Apps?
We optimize web applications with WebAssembly where it matters. Contact us to discuss your project.