Contents
開発環境の構築と前提条件
このセクションでは、Rust + WebAssembly を React/Vite プロジェクトで利用できるようにするために必要なツールとそのインストール手順をまとめます。各ツールは公式ドキュメントや 2025 年版の Qiita ガイド(Qiita 記事)で推奨されているものです。環境が整っていれば、以降の章で紹介する非同期 Rust コードをそのままブラウザ上で実行できます。
⚠️ Rust のバージョンについて
本稿では「Rust 1.91 以上」を目安にしていますが、執筆時点(2026‑05‑13)では最新の安定版は 1.78 系 です。将来的に 1.91 がリリースされた際に同等かそれ以降のバージョンであれば問題ありません。実際に使用する際はrustup update stableで最新の安定版を取得し、rustc --versionで確認してください。
必要なツール一覧
以下のツールがインストールされていれば、Rust → Wasm → Vite のフローをスムーズに進められます。各ツールは 公式サイト から取得することを推奨します。
- Rust(
rustup経由でインストール) - wasm-pack(
cargo install wasm-pack --locked推奨) - Node.js(LTS 系、v20 系が目安)と npm/pnpm などのパッケージマネージャ
- Vite(7.2 系以上、
npm create vite@latestでプロジェクト作成)
インストール手順
1️⃣ Rust ツールチェーンの導入
|
1 2 3 4 5 6 7 8 9 10 |
# rustup のインストーラを取得して実行 curl https://sh.rustup.rs -sSf | sh -s -- -y # デフォルトツールチェーンを stable に設定し、最新に更新 rustup default stable rustup update # バージョン確認(1.91 系が出ていればその数字、未リリースなら現在の安定版で OK) rustc --version # 例: rustc 1.78.0 (2024‑11‑14) |
2️⃣ wasm-pack のインストール
|
1 2 3 |
cargo install wasm-pack --locked wasm-pack --version # 確認表示例: wasm-pack 0.13.0 |
3️⃣ Node.js とパッケージマネージャの導入
公式サイト(https://nodejs.org/)から LTS 版をダウンロードし、インストール後にバージョンを確認します。
|
1 2 3 |
node --version # v20.x.x npm --version # 10.x 系 |
4️⃣ Vite プロジェクトの雛形作成
|
1 2 3 4 |
npm create vite@latest my-wasm-app -- --template react cd my-wasm-app npm install # 必要な依存パッケージをインストール |
以上で開発に必要な基盤は整いました。次は Rust 側のライブラリプロジェクトを作成します。
Rust プロジェクトの作成と WASM エクスポート設定
この章では、Rust で WebAssembly 用のライブラリ を作り、JavaScript から呼び出せる形にエクスポートするまでの手順を解説します。wasm-bindgen 系クレートが提供する型変換と #[wasm_bindgen] アトリビュートだけで、ほぼ自動的にインターフェイスが生成されます。
Cargo プロジェクトの初期化
まずはライブラリプロジェクトを作成し、必要な依存関係とビルドターゲットを設定します。
|
1 2 3 |
cargo new --lib async-wasm # ライブラリ用テンプレートで作成 cd async-wasm |
Cargo.toml の編集ポイント
| 項目 | 内容 | 補足 |
|---|---|---|
[dependencies] |
wasm-bindgen = "0.2"、js-sys = "0.3"、web-sys = { version = "0.3", features = [...] } |
web-sys はブラウザ API にアクセスするために必要な機能だけを列挙します。 |
[lib] |
crate-type = ["cdylib"] |
cdylib が .wasm バイナリ生成の必須設定です。 |
|
1 2 3 4 5 6 7 8 |
[dependencies] wasm-bindgen = "0.2" js-sys = "0.3" web-sys = { version = "0.3", features = ["Window", "Response", "RequestInit"] } [lib] crate-type = ["cdylib"] |
#[wasm_bindgen] の基本的な使い方
#[wasm_bindgen] が付いた public 関数は自動で JavaScript 側にエクスポートされます。引数・戻り値は JsValue か、wasm-bindgen が変換できる型(文字列、数値、配列等)に限られます。
|
1 2 3 4 5 6 7 8 |
use wasm_bindgen::prelude::*; /// JavaScript から呼び出せるシンプルな関数例 #[wasm_bindgen] pub fn greet(name: &str) -> String { format!("Hello, {}!", name) } |
ポイント
-pubが必須です。private関数はバインディングに含まれません。
-&str,String,i32,f64,JsValueなどは自動で変換されますが、独自構造体を渡す場合は#[wasm_bindgen]で別途定義が必要です。
非同期関数の実装と Promise への変換
ブラウザ上では非同期処理は Promise として扱われるため、Rust の async fn をそのままエクスポートするだけでは JavaScript 側で利用できません。wasm-bindgen-futures が提供するユーティリティを組み合わせて変換します。
async fn の実装例(fetch API)
以下は外部 API から JSON データを取得し、JsValue に変換して返す非同期関数です。エラーハンドリングも Result<_, JsValue> で行います。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
use wasm_bindgen::prelude::*; use wasm_bindgen_futures::{future_to_promise, JsFuture}; use web_sys::{Request, RequestInit, Response}; #[wasm_bindgen] pub async fn fetch_data(url: &str) -> Result<JsValue, JsValue> { // GET リクエストのオプション設定 let mut init = RequestInit::new(); init.method("GET"); init.mode(web_sys::RequestMode::Cors); // Request オブジェクト生成 let request = Request::new_with_str_and_init(url, &init)?; // window.fetch を呼び出し、Promise → Future に変換して await let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?; let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?; // Response へキャストし、JSON 本体を取得 let response: Response = resp_value.dyn_into()?; let json = JsFuture::from(response.json()?).await?; Ok(json) } |
future_to_promise で Promise にラップ
上記の async fn をそのままエクスポートすると JavaScript 側では Promise が返ってきません。そこで、公開用関数を別途作り future_to_promise で包みます。
|
1 2 3 4 5 6 7 8 9 |
use wasm_bindgen::prelude::*; use wasm_bindgen_futures::future_to_promise; #[wasm_bindgen] pub fn fetch_data_promise(url: String) -> js_sys::Promise { // async 関数を Future に変換し、Promise へラップ future_to_promise(async move { fetch_data(&url).await }) } |
ポイント
-Result<JsValue, JsValue>のOkはresolve、Errはrejectに自動マッピングされます。
- 引数は所有権を移すためにStringとし、内部で&strへ参照変換しています。
コンパイルから Vite + React への統合
この章では MDN が推奨するビルドフロー をベースに、生成された .wasm と JavaScript ラッパーを Vite プロジェクトに組み込む手順を詳述します。wasm-pack のオプションや package.json で必要な設定も合わせて解説します。
MDN 推奨のビルド手順(拡張版)
-
Wasm ターゲットを追加
bash
rustup target add wasm32-unknown-unknown -
デバッグビルドでコンパイル(オプション)
デバッグ時はcargo buildが速く、ローカルでの動作確認に便利です。
bash
cargo build --target wasm32-unknown-unknown -
本番用ビルドを
wasm-packでパッケージ化 -
--target webはブラウザ直接実行向け(ESM)。 --out-dir pkgで出力先ディレクトリを明示。--releaseフラグで最適化ビルド(デフォルトは debug)にする。--scope your-npm-scopeを付けると npm 公開時のスコープが自動設定されます。
bash
wasm-pack build --target web --out-dir pkg --release
package.jsonにビルドスクリプトとモジュール種別を追記
json
{
"name": "async-wasm",
"version": "0.1.0",
"type": "module", // ESM でインポートできるように必須
"scripts": {
"build:wasm": "cd async-wasm && wasm-pack build --target web --out-dir ../my-wasm-app/src/wasm-pkg --release"
},
"devDependencies": {
"binaryen": "^116.0.0" // wasm-opt 用にインストール
}
}
type: "module"によりimport文で拡張子.jsを省略でき、Vite のデフォルト設定と整合します。-
上記スクリプトは ルートディレクトリ から実行し、生成物を React アプリの
src/wasm-pkgに直接配置します。 -
最適化(任意)
bash
npx wasm-opt -Oz -o src/wasm-pkg/async_wasm_bg_opt.wasm src/wasm-pkg/async_wasm_bg.wasm
-Oz はサイズ最小化モードです。最適化後のファイル名はコード側でインポートパスを合わせるだけで OK です。
Vite プロジェクトでの WASM バンドルインポート
Vite はデフォルトで .wasm を アセット として扱うため、特別なプラグインは不要です。React コンポーネントからは動的 import() でラッパーを取得し、エクスポートされた関数を Promise 経由で呼び出します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// src/App.tsx import React, { useEffect, useState } from "react"; async function loadWasm() { // Vite のコード分割が自動的に .wasm をフェッチ const wasm = await import("./wasm-pkg/async_wasm.js"); return wasm; } export default function App() { const [data, setData] = useState<string>("Loading..."); useEffect(() => { (async () => { try { const wasm = await loadWasm(); // Promise 化された Rust 関数を呼び出す const json = await wasm.fetch_data_promise( "https://api.publicapis.org/entries" ); setData(JSON.stringify(json, null, 2)); } catch (e) { console.error(e); setData("Error loading WASM"); } })(); }, []); return ( <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-all" }}> {data} </pre> ); } |
import("./wasm-pkg/async_wasm.js")は 遅延ロード を意味し、最初のページ描画時に WASM 本体はまだダウンロードされません。- Vite が生成する
manifest.jsonと組み合わせると、キャッシュ戦略や CDN 配信も容易です。
デバッグ・最適化とサンプル実装
本番環境では バイナリサイズ と 起動レイテンシ が重要です。この章ではメモリ割り当ての軽量化、wasm-opt による圧縮手順、および Chrome DevTools を用いたプロファイル取得方法を解説します。
最適化手段
| 手法 | 設定例 | 効果(目安) |
|---|---|---|
| wee_alloc(小型アロケータ) | Cargo.toml に wee_alloc = { version = "0.4", optional = true }、features = ["wee_alloc"] を追加し、lib.rs でグローバルアロケータを有効化 |
バイナリサイズが約 10 % 減少 |
| wasm-opt -Oz(Binaryen) | npm install -g binaryen → wasm-opt -Oz -o async_wasm_opt.wasm pkg/async_wasm_bg.wasm |
サイズ最小化、ロード時間短縮 |
インラインヒント (#[inline]) |
重い計算関数に付与 | 関数呼び出しオーバーヘッド削減(CPU バウンドの場合) |
wee_alloc の有効化コード例
|
1 2 3 4 5 |
// lib.rs の先頭に追加 #[cfg(feature = "wee_alloc")] #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; |
Cargo ビルド時は --features wee_alloc を付与するか、デフォルト機能で有効化します。
パフォーマンス測定ポイント
| 項目 | 測定手法 | 推奨閾値 |
|---|---|---|
| Wasm 初回ロード時間 | Chrome DevTools の Network タブ → async_wasm_bg.wasm の Waterfall 時間 |
< 150 ms |
| メモリ使用量 | Performance > Memory プロファイル | < 5 MB (SPA 全体) |
| 非同期呼び出しレイテンシ | performance.now() 前後で計測 |
< 30 ms |
測定例(React コンポーネント内)
tsx
const t0 = performance.now();
await wasm.fetch_data_promise(url);
console.log(fetch latency: ${performance.now() - t0} ms);
画像処理サンプル:グレースケール変換
以下はブラウザ上の Canvas ピクセルデータを受け取り、Rust 側で高速にグレイスケール化して返す例です。非同期化すれば UI スレッドへの負荷も回避できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
use wasm_bindgen::prelude::*; use web_sys::{ImageData, CanvasRenderingContext2d}; #[wasm_bindgen] pub fn grayscale(data: ImageData) -> Result<ImageData, JsValue> { // ピクセルバッファを取得(Uint8ClampedArray → Vec<u8>) let mut pixels = data.data().to_vec(); // 4 バイトごとに RGB を計算し、同じ値で上書き for i in (0..pixels.len()).step_by(4) { let r = pixels[i] as f32; let g = pixels[i + 1] as f32; let b = pixels[i + 2] as f32; // NTSC 加重平均(人間の視覚特性に合わせた係数) let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8; pixels[i] = gray; pixels[i + 1] = gray; pixels[i + 2] = gray; } // 新しい ImageData を生成して返す ImageData::new_with_u8_clamped_array_and_width( wasm_bindgen::Clamped(&mut pixels), data.width(), ) } |
React 側からの呼び出し例(非同期化版):
|
1 2 3 4 5 6 7 8 9 10 |
import { useRef, useEffect } from "react"; async function applyGrayScale(ctx: CanvasRenderingContext2D) { const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); const wasm = await import("./wasm-pkg/async_wasm.js"); // Promise 化された関数を利用(重い画像は別スレッドで実行) const result = await wasm.grayscale(imgData); ctx.putImageData(result, 0, 0); } |
まとめと次のアクション
- 環境構築:Rust (最新安定版)、wasm-pack、Node.js v20 系、Vite7.2+ をインストールし、
my-wasm-appの雛形を作成しました。 - Rust 側実装:
#[wasm_bindgen]とfuture_to_promiseを組み合わせて非同期関数を Promise に変換し、ブラウザから呼び出せるようにしました。 - ビルドフロー:MDN 推奨の
wasm-pack build --target web --releaseに加えて、package.jsonのtype: "module"設定と最適化スクリプトを用意し、Vite へシームレスに組み込みました。 - 最適化 & デバッグ:
wee_allocとwasm-opt -Ozによるサイズ削減、Chrome DevTools を使ったロード時間・メモリ測定手順、画像処理サンプルを提示しました。
今すぐできること
- ビルドスクリプト実行
bash
npm run build:wasm # Rust → Wasm の一括生成
npm run dev # Vite 開発サーバ起動 - 非同期ロジックを拡張
- REST API だけでなく WebSocket、IndexedDB へのアクセスも
async fn+future_to_promiseパターンで実装できます。 - サイズ削減の継続的改善
- CI に
wasm-optを組み込み、プルリクエストごとに最適化済みバイナリを生成し、GitHub Actions のキャッシュでビルド時間を短縮しましょう。
これらの手順を踏めば、React/Vite プロジェクトに 高速かつ安全な非同期 Rust+Wasm コンポーネント を本番レベルで組み込むことが可能です。ぜひ自分のアプリケーションに合わせてカスタマイズし、次世代 Web 開発を体感してください。