Skip to main content
Back to Blog
Tutorials
3 min read
January 26, 2025

How to Implement WebAssembly Modules in Next.js

Use WebAssembly in Next.js for compute-intensive tasks with Rust compilation, async module loading, and typed bindings.

Ryel Banfield

Founder & Lead Developer

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.

WebAssemblyWASMperformanceRustNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles