WebAssembly (WASM) 入門 — ブラウザで重い処理を実行する
WebAssembly(WASM)を使うと、ブラウザ上で C++・Rust などのネイティブコードをほぼネイティブ速度で実行できます。この記事では WASM の仕組みから Rust での実装・JavaScript への組み込み・Godot/Unity の Web 出力との関係まで解説します。
WebAssembly とは
WebAssembly(WASM)は W3C が標準化したバイナリ形式の命令セットです。ブラウザの JavaScript エンジン上で動作し、C・C++・Rust など多くの言語からコンパイルできます。
JavaScript と WASM の比較
| 項目 | JavaScript | WebAssembly |
|---|---|---|
| 形式 | テキスト(動的型付け) | バイナリ(静的型付け) |
| 実行速度 | JIT コンパイルで高速 | ネイティブに近い速度 |
| 起動速度 | 速い | やや遅い(解析コスト) |
| メモリ管理 | GC(自動) | 線形メモリ(手動 or 言語依存) |
| DOM 操作 | 直接可能 | JS 経由が必要 |
| 対応ブラウザ | 全主要ブラウザ | 全主要ブラウザ(2017年〜) |
| ユースケース | 汎用 | 計算集中型処理 |
WASM が得意な処理
- 画像・動画処理: フィルタ・エンコード・デコード
- 音声処理: リアルタイムエフェクト・FFT
- 暗号処理: ハッシュ計算・暗号化
- 物理シミュレーション: 流体・剛体
- ゲームエンジン: Godot・Unity の Web 出力
- 機械学習推論: ONNX Runtime WebAssembly
WASM の仕組み
ソースコード (Rust / C++ / C)
↓ コンパイル(wasm-pack / emcc)
.wasm ファイル(バイナリ)+ JS グルーコード
↓ ブラウザが読み込み
WebAssembly モジュール(メモリ・関数テーブル)
↓
JavaScript から関数を呼び出す
WASM モジュールは線形メモリ(1 次元のバイト配列)を持ち、JS と WASM の間でデータをやり取りする際はこのメモリを介します。
Rust → WASM のセットアップ(wasm-pack)
必要なツール
# Rust のインストール
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# WASM ターゲットの追加
rustup target add wasm32-unknown-unknown
# wasm-pack のインストール
cargo install wasm-pack
# バージョン確認
wasm-pack --version
プロジェクトの作成
# wasm-pack テンプレートで新規プロジェクト
cargo new --lib wasm-hello
cd wasm-hello
Cargo.toml を編集します。
[package]
name = "wasm-hello"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
[profile.release]
opt-level = "s" # サイズ最適化
Rust コードを書く
// src/lib.rs
use wasm_bindgen::prelude::*;
// JavaScript から呼び出せる関数
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}! From WebAssembly.", name)
}
// 重い計算処理の例: フィボナッチ数列
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
match n {
0 => 0,
1 => 1,
_ => {
let mut a: u64 = 0;
let mut b: u64 = 1;
for _ in 2..=n {
let tmp = a + b;
a = b;
b = tmp;
}
b
}
}
}
// 画像処理の例: グレースケール変換
#[wasm_bindgen]
pub fn grayscale(pixels: &mut [u8]) {
// RGBA 形式のピクセルデータをグレースケールに変換
for chunk in pixels.chunks_mut(4) {
let r = chunk[0] as f32;
let g = chunk[1] as f32;
let b = chunk[2] as f32;
let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
chunk[0] = gray;
chunk[1] = gray;
chunk[2] = gray;
// chunk[3] はアルファ値なので変更しない
}
}
ビルド
# npm パッケージとしてビルド
wasm-pack build --target web
# または bundler 向け(webpack / Vite)
wasm-pack build --target bundler
# 出力先: pkg/
# ├── wasm_hello.js ← JS グルーコード
# ├── wasm_hello_bg.wasm ← WASM バイナリ
# ├── wasm_hello.d.ts ← TypeScript 型定義
# └── package.json
JavaScript / TypeScript から使う
バニラ JS(ブラウザ直接)
<!-- index.html -->
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<canvas id="canvas" width="400" height="400"></canvas>
<script type="module">
import init, { greet, fibonacci, grayscale } from "./pkg/wasm_hello.js";
async function main() {
// WASM モジュールを初期化
await init();
// 文字列を返す関数
console.log(greet("World"));
// → "Hello, World! From WebAssembly."
// 重い計算
console.log(fibonacci(50));
// → 12586269025
// 画像処理(Canvas API と組み合わせ)
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.onload = () => {
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, 400, 400);
// WASM でグレースケール変換
grayscale(imageData.data);
ctx.putImageData(imageData, 0, 0);
};
img.src = "photo.jpg";
}
main();
</script>
</body>
</html>
Vite + TypeScript での組み込み
npm create vite@latest my-wasm-app -- --template vanilla-ts
cd my-wasm-app
npm install
// src/main.ts
import init, { fibonacci } from "../pkg/wasm_hello.js";
async function run() {
await init();
const start = performance.now();
const result = fibonacci(45);
const end = performance.now();
console.log(`fibonacci(45) = ${result}`);
console.log(`実行時間: ${(end - start).toFixed(2)}ms`);
}
run();
vite.config.ts で WASM を有効化します。
import { defineConfig } from "vite"
export default defineConfig({
// Vite 5 以降は WASM がデフォルトでサポートされています
optimizeDeps: {
exclude: ["wasm-hello"]
}
})
Web Workers と組み合わせて UI をブロックしない
重い WASM 処理はメインスレッドをブロックします。Web Workers で別スレッドで実行するのがベストプラクティスです。
// worker.ts
import init, { fibonacci } from "../pkg/wasm_hello.js";
let initialized = false;
self.onmessage = async (e) => {
if (!initialized) {
await init();
initialized = true;
}
const { n } = e.data;
const result = fibonacci(n);
self.postMessage({ result });
};
// main.ts
const worker = new Worker(new URL("./worker.ts", import.meta.url), {
type: "module"
});
worker.postMessage({ n: 50 });
worker.onmessage = (e) => {
console.log("Result:", e.data.result);
};
Godot 4 の Web エクスポートと WASM
Godot 4 の Web エクスポートは GDScript・C#・GDExtension(C++)のコードをEmscripten を使って WASM にコンパイルします。
エクスポート手順
-
エクスポートテンプレートのダウンロード
エディタ→エクスポートテンプレートの管理→ダウンロード
-
Web エクスポートの設定
プロジェクト→エクスポート→追加→Web
-
必要な設定
Variant: Standard(通常)/ Thread(マルチスレッド対応)
Extensions Support: GDExtension の C++ コードがある場合に有効化
- エクスポート実行
godot --export-release "Web" build/index.html
Godot Web エクスポートの WASM 構造
build/
├── index.html ← エントリーポイント
├── index.js ← Emscripten グルーコード
├── index.wasm ← エンジン本体(数十 MB)
├── index.pck ← ゲームデータ(Packed file)
└── index.audio.worklet.js
SharedArrayBuffer の注意点
Godot の Thread(マルチスレッド)バリアントを使う場合、SharedArrayBuffer が必要です。これは COOP/COEP HTTP ヘッダーを設定しないと動作しません。
# サーバーの設定に必要なヘッダー
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
GitHub Pages では COOP/COEP ヘッダーが設定できないため、Thread バリアントは GitHub Pages での公開に向きません。
Godot 4.3 以降の対策: シングルスレッドエクスポートを選択すると
SharedArrayBuffer不要になります。またProgressive Web App > Enableオプションを有効にすると、Service Worker でヘッダーをシミュレートして GitHub Pages でも動作させられます。
Unity の Web ビルドと WASM
Unity の WebGL ビルドも同様に Emscripten で WASM を生成します。
ビルド設定:
File → Build Settings → WebGL → Switch Platform → Build
出力:
Build/
├── index.html
├── Build/
│ ├── game.loader.js
│ ├── game.data ← ゲームデータ
│ ├── game.framework.js ← Unity ランタイム
│ └── game.wasm ← Unity エンジン WASM
Unity WebGL の制限事項
- マルチスレッド: Unity 6 から WebAssembly スレッドの実験的サポート(COOP/COEP 必要)
- ファイルシステム: IndexedDB を使った仮想ファイルシステム
- ネイティブプラグイン:
.dllは使えない(JavaScript プラグインに変換必要)
ハマりやすいポイント
WASM のバイナリサイズが大きくなる
Rust の WASM バイナリはデフォルトで数 MB になることがあります。wasm-opt で最適化します。
# wasm-pack は自動で wasm-opt を実行するが、手動で行う場合
cargo install wasm-opt
wasm-opt pkg/wasm_hello_bg.wasm -Os -o pkg/wasm_hello_bg.wasm
# Cargo.toml でサイズ最適化
[profile.release]
opt-level = "s" # サイズ優先
lto = true # Link Time Optimization
JS ↔ WASM 間のデータ受け渡しコストに注意
WASM の線形メモリと JS のヒープは別物です。文字列や配列を渡すたびにコピーが発生します。頻繁に呼び出す場合はバッチ処理にまとめます。
// NG: 1 ピクセルずつ処理(JS → WASM のコール多数)
#[wasm_bindgen]
pub fn process_pixel(r: u8, g: u8, b: u8) -> u8 { ... }
// OK: バッファ全体を一度に渡す
#[wasm_bindgen]
pub fn process_image(pixels: &mut [u8]) { ... }
WASM は同期的にロードできない
init() は非同期(Promise)を返します。await を忘れると関数呼び出しが失敗します。
// NG: await なし
import init, { fibonacci } from "./pkg/wasm_hello.js";
init();
fibonacci(10); // エラー: モジュールが初期化されていない
// OK
await init();
fibonacci(10);
Godot Web エクスポートの MIME タイプ設定
.wasm ファイルを配信するサーバーは Content-Type: application/wasm を返す必要があります。設定がないとブラウザがエラーを出します。
# nginx の設定例
types {
application/wasm wasm;
}
まとめ
- WebAssembly は C++・Rust などをブラウザでネイティブ速度で実行する標準技術
- Rust + wasm-pack で比較的簡単に WASM モジュールを作成し JS から呼び出せる
- 重い処理は Web Workers と組み合わせて UI をブロックしないよう設計する
- Godot 4・Unity の Web エクスポートは Emscripten 経由で WASM を生成している
- SharedArrayBuffer(マルチスレッド)使用時は COOP/COEP ヘッダー設定が必須