•14 min read

RustとWebAssemblyでReactを強化する:包括的ガイド

RustとWebAssemblyでReactを強化する:包括的ガイド

現代のウェブ開発において、ブラウザでスムーズに動作する高性能なアプリケーションを構築することは、これまで以上に重要になっています。ウェブアプリケーションは、動画編集や画像処理からリアルタイム3Dレンダリング、科学シミュレーションまで、あらゆるものを扱うようになり、ますます複雑化しています。それに伴い、JavaScriptの限界がより顕著になってきています。JavaScriptエンジンは信じられないほど最適化されていますが、根本的には解釈型の動的型付け言語を実行しています。CPU負荷の高い計算集約的なタスクに関しては、より低レベルなものが必要になります。

そこで登場するのがWebAssembly(Wasm)です。WebAssemblyは、スタックベースの仮想マシン用のバイナリ命令フォーマットであり、プログラミング言語のポータブルなコンパイルターゲットとして設計されています。これにより、C、C++、Rustなどの言語で書かれたコードを、ウェブブラウザでほぼネイティブな速度で実行できるようになり、クライアントおよびサーバーアプリケーションとしてウェブにデプロイできます。

WebAssemblyにコンパイルできる言語の中でも、Rustが最高の選択肢として浮上しています。Rustは、ガベージコレクションなしでメモリ安全性を実現し、比類のないパフォーマンスと、非常に歓迎的なエコシステムを提供します。さらに、WebAssembly向けのRustのツール、特にwasm-packは、コードのコンパイルとバンドルを非常に簡単にしてくれます。この包括的なガイドでは、計算コストの高いアルゴリズムをRustで記述し、wasm-packを使用してWebAssemblyにコンパイルし、そのWebAssemblyモジュールをReactフロントエンドからシームレスに呼び出すまでの全プロセスを順を追って説明します。

Audio Briefing
0:00 / 0:00

なぜRustとWebAssemblyなのか?

コードに入る前に、この組み合わせがなぜこれほど強力なのかを理解しておく価値があります。Rustはメモリ安全性とスレッド安全性を保証し、コンパイル時に(ヌルポインタの逆参照やバッファオーバーフローのような)あらゆる種類のバグを防ぎます。これは、CやC++アプリケーションをしばしば悩ませる壊滅的なメモリエラーを心配することなく、非常に高速なコードを書けることを意味します。

一方、WebAssemblyはサンドボックスと実行環境を提供します。RustをWebAssemblyにコンパイルすると、結果として得られるバイナリはコンパクトで高度に最適化されており、ブラウザの安全なサンドボックス内でJavaScriptコードと並行して実行されます。Reactアプリケーションは、重い処理をWebAssemblyモジュールにオフロードできるため、メインスレッドを解放し、UIの応答性を高く保つことができます。

Advertisement

RustとWebAssembly環境のセットアップ

始めるには、いくつかのツールをマシンにインストールする必要があります。スタックのReact部分については、Node.jsとnpm(またはyarn/pnpm)がすでにインストールされていることを前提としています。

まず、Rustの公式ツールチェインインストーラーであるrustupを使用してRustをインストールします。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

RustとCargo(Rustのパッケージマネージャー)がインストールされたら、RustコンパイラがWasmバイナリを生成できるように、WebAssemblyターゲットを追加する必要があります。

rustup target add wasm32-unknown-unknown

次に、wasm-packが必要です。このツールは、Rustで生成されたWebAssemblyをビルド、テスト、公開するための標準的な方法です。コンパイルプロセスを処理し、WasmモジュールをJavaScriptまたはTypeScriptプロジェクトに簡単にインポートできるように、必要なJavaScriptの「グルー」コードを生成します。

cargo install wasm-pack

Rust WebAssemblyプロジェクトの作成

ツールが揃ったので、新しいRustライブラリプロジェクトを作成しましょう。名前はrust-computeとします。

cargo new --lib rust-compute
cd rust-compute

Cargo.tomlファイルを開きます。プロジェクトを動的システムライブラリとしてコンパイルするように設定し(WebAssemblyに必要)、wasm-bindgen依存関係を追加する必要があります。wasm-bindgenクレートは、JavaScriptとRustの間のブリッジを提供し、複雑なデータ型(文字列や配列など)をWasm境界を越えて渡せるようにします。

[package]
name = "rust-compute"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2.84"

次に、計算負荷の高いRustコードを記述しましょう。src/lib.rsを開きます。素朴な再帰アプローチを使用してN番目のフィボナッチ数を計算する関数を記述します。これはフィボナッチ数を計算する最も効率的な方法ではありませんが、意図的に重いCPU負荷を生成するため、WebAssemblyのJavaScriptに対するパフォーマンス上の利点を示すのに最適な候補となります。

use wasm_bindgen::prelude::*;

// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator to keep the WebAssembly binary small.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

/// A computationally expensive recursive Fibonacci calculator.
/// We expose this to JavaScript using the #[wasm_bindgen] attribute.
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
    if n <= 1 {
        return n;
    }
    fibonacci(n - 1) + fibonacci(n - 2)
}

/// A more complex example: calculating prime numbers up to a given limit.
#[wasm_bindgen]
pub fn count_primes(limit: u32) -> u32 {
    let mut count = 0;
    for i in 2..=limit {
        if is_prime(i) {
            count += 1;
        }
    }
    count
}

fn is_prime(n: u32) -> bool {
    if n <= 1 {
        return false;
    }
    for i in 2..=(n as f64).sqrt() as u32 {
        if n % i == 0 {
            return false;
        }
    }
    true
}

ここで魔法を起こすのが#[wasm_bindgen]属性です。この属性で公開関数をアノテーションすることで、wasm-packはReactアプリからこれらの関数を直接呼び出すために必要なJavaScriptラッパーを自動的に生成します。

RustからWebAssemblyへのコンパイル

Rustコードをフロントエンドで利用できるWebAssemblyモジュールにコンパイルするには、wasm-packを使用します。ReactアプリケーションでViteまたはWebpackを使用するため、バンドラーをターゲットにしてビルドします。

wasm-pack build --target web

あるいは、古いWebpackセットアップを使用している場合は、--target bundlerを使用することもできます。しかし、最新のViteベースのReactセットアップでは、標準のESモジュールを使用してブラウザでモジュールを直接非同期にインスタンス化できるため、--target webが強く推奨されます。

ビルドが完了すると、Rustプロジェクトに新しいpkg/ディレクトリが作成されます。このフォルダには、コンパイルされた.wasmバイナリファイル、生成された.jsラッパー、およびオートコンプリートと型安全性のためのTypeScript .d.ts宣言ファイルが含まれています。

Advertisement

Reactフロントエンドのセットアップ

次に、Reactアプリケーションを作成しましょう。Viteを使用します。Viteは非常に高速な開発環境と、WebAssemblyを含む最新のウェブ標準に対する優れたサポートを提供します。

rust-computeフォルダの隣のディレクトリで、新しいReactプロジェクトをスキャフォールドします。

npm create vite@latest react-wasm-app -- --template react-ts
cd react-wasm-app
npm install

WebAssemblyパッケージのインポートを簡単にするために、コンパイル済みのpkgディレクトリをローカル依存関係として直接インストールできます。

npm install ../rust-compute/pkg

Viteでは、ESモジュールを使用してWebAssemblyをロードするのは簡単ですが、関数を呼び出す前にモジュールが初期化されていることを確認する必要があります。

ReactからWebAssemblyを呼び出す

WebAssembly関数をReact UIに統合しましょう。ユーザーが数値を入力し、計算コストの高いRust関数をトリガーし、その実行にかかる時間を測定できるコンポーネントを作成します。

src/App.tsxを開き、コードを次のように更新します。

import React, { useState, useEffect } from 'react';
// Import the default initialization function and our specific functions
import init, { fibonacci, count_primes } from 'rust-compute';
import './App.css';

function App() {
  const [isWasmLoaded, setIsWasmLoaded] = useState(false);
  const [fibInput, setFibInput] = useState<number>(40);
  const [primeInput, setPrimeInput] = useState<number>(100000);
  const [result, setResult] = useState<string | null>(null);
  const [executionTime, setExecutionTime] = useState<number | null>(null);

  // Initialize the WebAssembly module when the component mounts
  useEffect(() => {
    const loadWasm = async () => {
      try {
        // init() fetches and instantiates the .wasm file
        await init();
        setIsWasmLoaded(true);
        console.log("WebAssembly module loaded successfully.");
      } catch (error) {
        console.error("Failed to load WebAssembly module:", error);
      }
    };

    loadWasm();
  }, []);

  const handleCalculateFibonacci = () => {
    if (!isWasmLoaded) return;
    
    const startTime = performance.now();
    const answer = fibonacci(fibInput);
    const endTime = performance.now();
    
    setResult(`Fibonacci(${fibInput}) = ${answer}`);
    setExecutionTime(endTime - startTime);
  };

  const handleCalculatePrimes = () => {
    if (!isWasmLoaded) return;
    
    const startTime = performance.now();
    const answer = count_primes(primeInput);
    const endTime = performance.now();
    
    setResult(`Total primes up to ${primeInput} = ${answer}`);
    setExecutionTime(endTime - startTime);
  };

  return (
    <div className="App">
      <h1>React + Rust WebAssembly</h1>
      <p>High-performance computing in the browser.</p>
      
      {!isWasmLoaded ? (
        <p>Loading WebAssembly...</p>
      ) : (
        <div className="compute-section">
          <div className="card">
            <h2>Fibonacci Calculator</h2>
            <input 
              type="number" 
              value={fibInput} 
              onChange={(e) => setFibInput(Number(e.target.value))} 
            />
            <button onClick={handleCalculateFibonacci}>Calculate (Rust)</button>
          </div>

          <div className="card">
            <h2>Prime Counter</h2>
            <input 
              type="number" 
              value={primeInput} 
              onChange={(e) => setPrimeInput(Number(e.target.value))} 
            />
            <button onClick={handleCalculatePrimes}>Count Primes (Rust)</button>
          </div>

          {result && (
            <div className="results">
              <h3>Result: {result}</h3>
              {executionTime !== null && (
                <p>Execution Time: <strong>{executionTime.toFixed(2)} ms</strong></p>
              )}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

export default App;

統合の理解

上記のReactコンポーネントでは、useEffectフックを使用して、wasm-packによって提供されるinit()関数を介してWebAssemblyモジュールを非同期にロードおよびインスタンス化しています。WebAssembly関数を呼び出そうとする前に、この初期化が完了するのを待つことが重要です。そうしないと、アプリケーションはエラーをスローします。

モジュールがロードされると、handleCalculateFibonacciおよびhandleCalculatePrimes関数は、まるでネイティブのJavaScript関数であるかのようにRustコードを呼び出します。舞台裏では、wasm_bindgenによって生成されたJavaScriptラッパーが、JavaScript仮想マシンとWebAssembly環境間の境界を越えて数値をマーシャリングする処理を行います。実行時間を正確に測定するために、performance.now()呼び出しで実行をラップしています。これらの再帰関数の純粋なJavaScript版を実装すると、WebAssembly実装が解釈されたJavaScriptを劇的に上回る、顕著なパフォーマンス差に気づくでしょう。

まとめ

ReactとRustをWebAssembly経由で組み合わせることで、ウェブアプリケーションのパフォーマンスが新たなレベルに引き上げられます。wasm-packを利用することで、開発者は計算負荷の高いロジックをRustでシームレスに記述し、安全なバイナリ形式にコンパイルし、フロントエンドのJavaScriptアーキテクチャからネイティブに呼び出すことができます。

このハイブリッドアプローチにより、ユーザーインターフェース構築のためのReactの迅速な開発サイクルと豊富なエコシステムを維持しつつ、複雑な計算、シミュレーション、またはデータ処理タスクをRustに戦略的に委譲できます。ウェブアプリケーションがネイティブデスクトップソフトウェアの本格的なプラットフォーム代替へと進化し続ける中、RustとWebAssemblyのツールチェインは、次世代のウェブの基礎となる柱として間違いなく機能するでしょう。

こちらもおすすめ

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement