Documentation Next.js

はじめに

この記事では、Next.jsプロジェクトにWebAssembly(WASM)を導入し、高速な処理を実現する方法を解説します。

WebAssembly(WASM) とは、ブラウザ上で実行できるバイナリ形式のコードです。RustやC、C++などの言語で書かれたプログラムをコンパイルすることで、JavaScriptよりも高速な処理を実現できます。

この記事で学べること

  • WebAssemblyの基本概念と利点
  • Rust環境のセットアップとwasm-packの導入
  • Next.jsでWebAssemblyモジュールを読み込む方法
  • 実践的なユースケースと最適化テクニック

対象読者

  • Next.jsの基本を理解している開発者
  • パフォーマンス最適化に興味がある方
  • RustやWebAssemblyに初めて触れる方

WebAssemblyとは

基本概念

WebAssembly(WASM)は、Webブラウザで実行可能な低レベルのバイナリ形式です。主な特徴は以下のとおりです。

特徴説明
高速実行ネイティブに近い速度で実行可能
言語非依存Rust、C、C++、Go など多くの言語からコンパイル可能
サンドボックスセキュアな実行環境で動作
ポータブル主要なブラウザすべてで動作

JavaScriptとの比較

処理速度の比較(概念図):

JavaScript:  [====      ] 約40%
WebAssembly: [========= ] 約90%
ネイティブ:   [==========] 100%

※ 実際の速度は処理内容により異なります

WebAssemblyが特に効果を発揮するのは以下のような処理です。

  • 数値計算: 物理シミュレーション、科学計算
  • 画像・動画処理: フィルター適用、エンコード/デコード
  • 暗号化処理: ハッシュ計算、暗号化/復号化
  • ゲームロジック: 物理エンジン、AI処理

開発環境のセットアップ

Rustのインストール

まず、Rust言語をインストールします。RustはWebAssemblyとの親和性が高く、公式にWASM出力をサポートしています。

# Rustをインストール(Windows/macOS/Linux共通)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# インストール確認
rustc --version
# 出力例: rustc 1.75.0 (82e1608df 2023-12-21)

# WebAssemblyターゲットを追加
rustup target add wasm32-unknown-unknown

wasm-packのインストール

wasm-pack は、RustコードをWebAssemblyにコンパイルし、npmパッケージとして利用可能な形式に変換するツールです。

# wasm-packをインストール
cargo install wasm-pack

# インストール確認
wasm-pack --version
# 出力例: wasm-pack 0.12.1

Rustプロジェクトの作成

プロジェクト構成

Next.jsプロジェクトのルートディレクトリに、Rustのライブラリプロジェクトを作成します。

# Rustライブラリプロジェクトを作成
cargo new --lib wasm-lib
cd wasm-lib

作成後のディレクトリ構造は以下のようになります。

my-nextjs-app/
├── src/                  # Next.jsのソースコード
├── public/
├── wasm-lib/             # Rustプロジェクト(新規作成)
│   ├── Cargo.toml        # Rust依存関係の設定
│   └── src/
│       └── lib.rs        # Rustのソースコード
├── package.json
└── next.config.js

Cargo.tomlの設定

wasm-lib/Cargo.tomlを以下のように編集します。

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

# WebAssemblyライブラリとしてビルドするための設定
[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
# JavaScript/TypeScriptとの連携を可能にするライブラリ
wasm-bindgen = "0.2"

# コンソールログ出力用(オプション)
web-sys = { version = "0.3", features = ["console"] }

# パニック時のエラー表示を改善(開発時に便利)
console_error_panic_hook = { version = "0.1", optional = true }

[features]
default = ["console_error_panic_hook"]

# リリースビルド時の最適化設定
[profile.release]
# サイズ最適化を優先
opt-level = "s"
# リンク時最適化を有効化
lto = true

Rustコードの実装

wasm-lib/src/lib.rsに、WebAssemblyとしてエクスポートする関数を実装します。

// wasm-bindgenをインポート
// JavaScriptとRustの間でデータをやり取りするために必要
use wasm_bindgen::prelude::*;

// パニック時のエラー表示を改善(開発時のデバッグに役立つ)
#[cfg(feature = "console_error_panic_hook")]
pub fn set_panic_hook() {
    console_error_panic_hook::set_once();
}

// #[wasm_bindgen]属性を付けた関数はJavaScriptから呼び出し可能
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    // Rustの文字列フォーマット機能を使用
    format!("Hello, {}! This message is from WebAssembly.", name)
}

// 数値計算の例:フィボナッチ数列(再帰なし、高速版)
#[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 sum_array(numbers: &[i32]) -> i64 {
    // イテレータを使用して効率的に合計を計算
    numbers.iter().map(|&x| x as i64).sum()
}

// 文字列処理の例:文字列を逆順にする
#[wasm_bindgen]
pub fn reverse_string(input: &str) -> String {
    input.chars().rev().collect()
}

// 素数判定の例
#[wasm_bindgen]
pub fn is_prime(n: u64) -> bool {
    if n < 2 {
        return false;
    }
    if n == 2 {
        return true;
    }
    if n % 2 == 0 {
        return false;
    }

    let sqrt_n = (n as f64).sqrt() as u64;
    for i in (3..=sqrt_n).step_by(2) {
        if n % i == 0 {
            return false;
        }
    }

    true
}

// 指定範囲の素数をすべて取得
#[wasm_bindgen]
pub fn get_primes_in_range(start: u64, end: u64) -> Vec<u64> {
    (start..=end).filter(|&n| is_prime(n)).collect()
}

WebAssemblyへのコンパイル

Rustコードをwebパッケージとしてコンパイルします。

cd wasm-lib

# Webターゲット用にビルド(ESモジュール形式)
wasm-pack build --target web

# ビルド成功後、pkgディレクトリが作成される
ls pkg/
# 出力例:
# wasm_lib.d.ts      # TypeScript型定義
# wasm_lib.js        # JavaScriptグルーコード
# wasm_lib_bg.wasm   # WebAssemblyバイナリ
# package.json

ビルドオプションの説明:

オプション説明
--target webブラウザで直接使用可能な形式
--target bundlerwebpack等のバンドラー向け
--target nodejsNode.js向け

Next.jsへの統合

Next.js設定の更新

next.config.js(またはnext.config.mjs)でWebAssemblyサポートを有効にします。

// next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  // WebAssemblyの実験的機能を有効化
  webpack: (config, { isServer }) => {
    // WebAssemblyの非同期読み込みを有効化
    config.experiments = {
      ...config.experiments,
      asyncWebAssembly: true,
    };

    // サーバーサイドでのWASMファイルの取り扱い設定
    if (isServer) {
      config.output.webassemblyModuleFilename = './../static/wasm/[modulehash].wasm';
    } else {
      config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm';
    }

    return config;
  },
};

module.exports = nextConfig;

WASMモジュールをプロジェクトに追加

ビルドしたWASMパッケージをNext.jsプロジェクトで使用できるようにします。

# プロジェクトルートに戻る
cd ..

# pkgディレクトリをsrc配下にコピー(またはシンボリックリンク作成)
cp -r wasm-lib/pkg src/wasm

# または、package.jsonに依存関係として追加
# npm install ./wasm-lib/pkg

基本的な使用例

まず、WASMモジュールを読み込むカスタムフックを作成します。

// src/hooks/useWasm.ts
'use client';

import { useState, useEffect } from 'react';

// WASMモジュールの型定義
interface WasmModule {
  greet: (name: string) => string;
  fibonacci: (n: number) => bigint;
  sum_array: (numbers: Int32Array) => bigint;
  reverse_string: (input: string) => string;
  is_prime: (n: bigint) => boolean;
  get_primes_in_range: (start: bigint, end: bigint) => BigUint64Array;
}

export function useWasm() {
  const [wasm, setWasm] = useState<WasmModule | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let mounted = true;

    async function loadWasm() {
      try {
        // WASMモジュールを動的にインポート
        const wasmModule = await import('../wasm/wasm_lib');

        // 初期化が必要な場合(wasm-packの設定による)
        if (wasmModule.default) {
          await wasmModule.default();
        }

        if (mounted) {
          setWasm(wasmModule as unknown as WasmModule);
          setLoading(false);
        }
      } catch (err) {
        if (mounted) {
          setError(err instanceof Error ? err : new Error('Failed to load WASM'));
          setLoading(false);
        }
      }
    }

    loadWasm();

    return () => {
      mounted = false;
    };
  }, []);

  return { wasm, loading, error };
}

Reactコンポーネントでの使用

// src/components/WasmDemo.tsx
'use client';

import { useState } from 'react';
import { useWasm } from '../hooks/useWasm';

export function WasmDemo() {
  const { wasm, loading, error } = useWasm();
  const [name, setName] = useState('Next.js Developer');
  const [fibInput, setFibInput] = useState(40);
  const [results, setResults] = useState<{
    greeting?: string;
    fibonacci?: string;
    executionTime?: number;
  }>({});

  // 読み込み中の表示
  if (loading) {
    return (
      <div className="p-4 bg-gray-100 rounded">
        <p>WebAssemblyモジュールを読み込み中...</p>
      </div>
    );
  }

  // エラー表示
  if (error) {
    return (
      <div className="p-4 bg-red-100 rounded">
        <p className="text-red-600">エラー: {error.message}</p>
      </div>
    );
  }

  // 挨拶関数のテスト
  const handleGreet = () => {
    if (wasm) {
      const greeting = wasm.greet(name);
      setResults(prev => ({ ...prev, greeting }));
    }
  };

  // フィボナッチ計算(パフォーマンス計測付き)
  const handleFibonacci = () => {
    if (wasm) {
      const startTime = performance.now();
      const result = wasm.fibonacci(fibInput);
      const endTime = performance.now();

      setResults(prev => ({
        ...prev,
        fibonacci: result.toString(),
        executionTime: endTime - startTime,
      }));
    }
  };

  return (
    <div className="space-y-6 p-6 bg-white rounded-lg shadow">
      <h2 className="text-2xl font-bold">WebAssemblyデモ</h2>

      {/* 挨拶テスト */}
      <div className="space-y-2">
        <h3 className="text-lg font-semibold">Greet関数</h3>
        <div className="flex gap-2">
          <input
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            className="border rounded px-3 py-2 flex-1"
            placeholder="名前を入力"
          />
          <button
            onClick={handleGreet}
            className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
          >
            実行
          </button>
        </div>
        {results.greeting && (
          <p className="bg-gray-100 p-3 rounded">{results.greeting}</p>
        )}
      </div>

      {/* フィボナッチ計算 */}
      <div className="space-y-2">
        <h3 className="text-lg font-semibold">フィボナッチ数列</h3>
        <div className="flex gap-2">
          <input
            type="number"
            value={fibInput}
            onChange={(e) => setFibInput(Number(e.target.value))}
            className="border rounded px-3 py-2 w-24"
            min="0"
            max="90"
          />
          <button
            onClick={handleFibonacci}
            className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
          >
            計算
          </button>
        </div>
        {results.fibonacci && (
          <div className="bg-gray-100 p-3 rounded">
            <p>F({fibInput}) = {results.fibonacci}</p>
            <p className="text-sm text-gray-600">
              実行時間: {results.executionTime?.toFixed(3)}ms
            </p>
          </div>
        )}
      </div>
    </div>
  );
}

ページでの使用

// src/app/wasm-demo/page.tsx
import { WasmDemo } from '../../components/WasmDemo';

export default function WasmDemoPage() {
  return (
    <main className="container mx-auto py-8">
      <h1 className="text-3xl font-bold mb-8">
        Next.js + WebAssembly デモ
      </h1>
      <WasmDemo />
    </main>
  );
}

実践的なユースケース

画像処理の例

画像のグレースケール変換をWebAssemblyで実装する例です。

// wasm-lib/src/lib.rs に追加

// 画像処理:RGBAピクセルデータをグレースケールに変換
#[wasm_bindgen]
pub fn to_grayscale(data: &mut [u8]) {
    // RGBAなので4バイトずつ処理
    for chunk in data.chunks_exact_mut(4) {
        // 輝度計算(人間の目の感度に基づく重み付け)
        let gray = (0.299 * chunk[0] as f32
                  + 0.587 * chunk[1] as f32
                  + 0.114 * chunk[2] as f32) as u8;

        chunk[0] = gray; // R
        chunk[1] = gray; // G
        chunk[2] = gray; // B
        // chunk[3] はアルファ値なのでそのまま
    }
}

// 画像処理:コントラスト調整
#[wasm_bindgen]
pub fn adjust_contrast(data: &mut [u8], factor: f32) {
    for chunk in data.chunks_exact_mut(4) {
        for i in 0..3 {
            let value = chunk[i] as f32;
            // 128を中心にコントラストを調整
            let adjusted = ((value - 128.0) * factor + 128.0).clamp(0.0, 255.0);
            chunk[i] = adjusted as u8;
        }
    }
}
// Reactでの画像処理コンポーネント
'use client';

import { useRef, useState } from 'react';
import { useWasm } from '../hooks/useWasm';

export function ImageProcessor() {
  const { wasm, loading } = useWasm();
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [imageLoaded, setImageLoaded] = useState(false);

  const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file || !canvasRef.current) return;

    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);
      setImageLoaded(true);
    };
    img.src = URL.createObjectURL(file);
  };

  const applyGrayscale = () => {
    if (!wasm || !canvasRef.current) return;

    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d')!;
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    // WebAssemblyでグレースケール変換を実行
    const startTime = performance.now();
    wasm.to_grayscale(imageData.data);
    const endTime = performance.now();

    ctx.putImageData(imageData, 0, 0);
    console.log(`グレースケール変換: ${(endTime - startTime).toFixed(2)}ms`);
  };

  if (loading) return <p>読み込み中...</p>;

  return (
    <div className="space-y-4">
      <input type="file" accept="image/*" onChange={handleImageUpload} />
      <canvas ref={canvasRef} className="border max-w-full" />
      {imageLoaded && (
        <button
          onClick={applyGrayscale}
          className="bg-purple-500 text-white px-4 py-2 rounded"
        >
          グレースケールに変換
        </button>
      )}
    </div>
  );
}

パフォーマンス最適化

WASMファイルサイズの最適化

# Cargo.toml に追加

[profile.release]
# サイズ最適化を最大化
opt-level = "z"
# リンク時最適化
lto = true
# デバッグ情報を除去
strip = true
# コードの生成単位を1に(最適化効果を高める)
codegen-units = 1

wasm-optによる追加最適化

# wasm-optをインストール(binaryenパッケージに含まれる)
npm install -g binaryen

# WASMファイルを最適化
wasm-opt -Oz -o optimized.wasm wasm_lib_bg.wasm

遅延読み込みの実装

// 必要な時だけWASMを読み込む
async function loadWasmOnDemand() {
  // ユーザーがボタンをクリックした時など、
  // 必要になったタイミングでWASMを読み込む
  const wasm = await import('../wasm/wasm_lib');
  await wasm.default();
  return wasm;
}

Web Workerでの実行

メインスレッドをブロックしないよう、重い処理はWeb Workerで実行できます。

// workers/wasm.worker.ts
self.onmessage = async (e) => {
  const { type, payload } = e.data;

  // WASMモジュールを読み込み
  const wasm = await import('../wasm/wasm_lib');
  await wasm.default();

  switch (type) {
    case 'fibonacci':
      const result = wasm.fibonacci(payload.n);
      self.postMessage({ type: 'result', result: result.toString() });
      break;
    // その他の処理...
  }
};

トラブルシューティング

よくあるエラーと解決策

エラー原因解決策
WebAssembly.instantiate(): expected magic wordWASMファイルが正しく読み込まれていないnext.config.jsの設定を確認
RuntimeError: memory access out of boundsメモリ領域外へのアクセスRust側のコードでバウンドチェックを追加
LinkError: import object field is not a Functionインポート関数が見つからないwasm-bindgenのバージョンを確認

デバッグのヒント

// Rust側でコンソールログを出力
use web_sys::console;

#[wasm_bindgen]
pub fn debug_function(value: i32) {
    // ブラウザのコンソールに出力
    console::log_1(&format!("Value: {}", value).into());
}

まとめ

この記事では、Next.jsにWebAssemblyを導入する方法を解説しました。

学んだこと

  1. WebAssemblyの基本 - 高速実行可能なバイナリ形式
  2. Rust環境のセットアップ - wasm-packを使ったビルド環境
  3. Next.jsへの統合 - 設定変更と動的インポート
  4. 実践的なユースケース - 画像処理などのCPU集約型タスク
  5. 最適化テクニック - ファイルサイズ削減とパフォーマンス向上

次のステップ

  • より複雑なRustライブラリの活用(画像処理ライブラリなど)
  • Server Componentsとの組み合わせ
  • エッジ関数でのWebAssembly活用

WebAssemblyを活用することで、Webアプリケーションの可能性が大きく広がります。ぜひ実際のプロジェクトで試してみてください。

参考文献

円