WebAssembly 入門 — ブラウザで重い処理をネイティブ速度で実行する
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
プロジェクトの作成
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::*;
#[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]) {
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(ブラウザ直接)
<script type="module">
import init, { greet, fibonacci, grayscale } from "./pkg/wasm_hello.js";
async function main() {
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 imageData = ctx.getImageData(0, 0, 400, 400);
grayscale(imageData.data);
ctx.putImageData(imageData, 0, 0);
}
main();
</script>
Vite + TypeScript での組み込み
// 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();
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 result = fibonacci(e.data.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# のコードを Emscripten を使って WASM にコンパイルします。
エクスポート手順
エディタ→エクスポートテンプレートの管理→ダウンロードプロジェクト→エクスポート→追加→Web- Variant を
Standard(通常)またはThread(マルチスレッド対応)から選択 - エクスポート実行
godot --export-release "Web" build/index.html
出力されるファイル構成:
build/
├── index.html ← エントリーポイント
├── index.js ← Emscripten グルーコード
├── index.wasm ← エンジン本体(数十 MB)
└── index.pck ← ゲームデータ
SharedArrayBuffer の注意点
Thread バリアントを使う場合は COOP/COEP HTTP ヘッダーが必要です。
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
GitHub Pages ではこのヘッダーを設定できないため、Thread バリアントは GitHub Pages に向きません。Godot 4.3 以降はシングルスレッドエクスポートを選択するか、Service Worker で対応できます。
ハマりやすいポイント
WASM のバイナリサイズが大きくなる
# Cargo.toml でサイズ最適化
[profile.release]
opt-level = "s" # サイズ優先
lto = true # Link Time Optimization
JS ↔ WASM 間のデータ受け渡しコストに注意
// 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 なし
init();
fibonacci(10); // エラー: モジュールが初期化されていない
// OK
await init();
fibonacci(10);
まとめ
- WebAssembly は C++・Rust などをブラウザでネイティブ速度で実行する標準技術
- Rust + wasm-pack で比較的簡単に WASM モジュールを作成し JS から呼び出せる
- 重い処理は Web Workers と組み合わせて UI をブロックしないよう設計する
- Godot 4・Unity の Web エクスポートは Emscripten 経由で WASM を生成している
- SharedArrayBuffer(マルチスレッド)使用時は COOP/COEP ヘッダー設定が必須