はじめに
「JavaScriptが遅い」という問題を解決するために登場したWebAssembly(Wasm)。
RustはWasm対応が優秀で、めっちゃ簡単にブラウザで動くバイナリが作れます。
実際に作ってみたらマジで速かったので、その方法を紹介します。
目次
WebAssemblyとは
WebAssembly(Wasm) は、ブラウザで動くバイナリフォーマットです。
特徴
- 高速: ネイティブに近い速度
- 安全: サンドボックス内で実行
- ポータブル: どのブラウザでも動く
- 言語非依存: C/C++, Rust, Go などからコンパイル可能
JavaScript vs WebAssembly
| 項目 | JavaScript | WebAssembly |
|---|---|---|
| 実行速度 | 遅い(JIT) | 速い(AOT) |
| ファイルサイズ | テキスト | バイナリ |
| 型システム | 動的 | 静的 |
| 用途 | DOM操作, UI | 計算処理 |
結論: 計算が重い処理はWasmに任せると良い。
環境構築
必要なツール
# wasm-pack のインストール
cargo install wasm-pack
# または直接ダウンロード
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
プロジェクト作成
cargo new wasm-example --lib
cd wasm-example
Cargo.toml
[package]
name = "wasm-example"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
[profile.release]
opt-level = "s" # サイズ最適化
lto = true # Link Time Optimization
Hello Wasm
Rustコード
// src/lib.rs
use wasm_bindgen::prelude::*;
// JavaScript から呼べる関数
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// JavaScript の console.log を呼ぶ
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub fn hello_wasm() {
log("Hello from Rust!");
}
ビルド
wasm-pack build --target web
生成されるファイル:
-
pkg/wasm_example_bg.wasm- Wasmバイナリ -
pkg/wasm_example.js- JavaScriptグルーコード -
pkg/wasm_example.d.ts- TypeScript型定義
HTMLで使う
<!DOCTYPE html>
<html>
<head>
<title>Rust Wasm Example</title>
</head>
<body>
<script type="module">
import init, { greet, hello_wasm } from './pkg/wasm_example.js';
async function main() {
await init();
hello_wasm(); // コンソールに "Hello from Rust!"
const message = greet("World");
document.body.textContent = message; // "Hello, World!"
}
main();
</script>
</body>
</html>
ローカルサーバーで確認
# Python
python -m http.server 8080
# Node.js
npx serve .
JavaScriptとの連携
基本的な型の受け渡し
use wasm_bindgen::prelude::*;
// 数値
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// 文字列
#[wasm_bindgen]
pub fn reverse_string(s: &str) -> String {
s.chars().rev().collect()
}
// 配列(Uint8Array)
#[wasm_bindgen]
pub fn sum_array(arr: &[u8]) -> u32 {
arr.iter().map(|&x| x as u32).sum()
}
構造体を公開
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct Point {
x: f64,
y: f64,
}
#[wasm_bindgen]
impl Point {
#[wasm_bindgen(constructor)]
pub fn new(x: f64, y: f64) -> Point {
Point { x, y }
}
pub fn distance(&self, other: &Point) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
// getter
#[wasm_bindgen(getter)]
pub fn x(&self) -> f64 {
self.x
}
// setter
#[wasm_bindgen(setter)]
pub fn set_x(&mut self, x: f64) {
self.x = x;
}
}
JavaScript側:
import { Point } from './pkg/wasm_example.js';
const p1 = new Point(0, 0);
const p2 = new Point(3, 4);
console.log(p1.distance(p2)); // 5
JavaScript の関数を呼ぶ
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
// alert
fn alert(s: &str);
// console.log
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
// document.getElementById
#[wasm_bindgen(js_namespace = document)]
fn getElementById(id: &str) -> JsValue;
}
#[wasm_bindgen]
pub fn show_alert() {
alert("Hello from Rust!");
}
パフォーマンス比較
テスト:フィボナッチ数列
JavaScript版
function fibJS(n) {
if (n <= 1) return n;
return fibJS(n - 1) + fibJS(n - 2);
}
Rust/Wasm版
#[wasm_bindgen]
pub fn fib_wasm(n: u32) -> u64 {
if n <= 1 {
n as u64
} else {
fib_wasm(n - 1) + fib_wasm(n - 2)
}
}
ベンチマーク結果(fib(40))
| 実装 | 時間 |
|---|---|
| JavaScript | 約1200ms |
| Wasm | 約400ms |
約3倍速い!
テスト:配列操作
JavaScript版
function sumArrayJS(arr) {
return arr.reduce((a, b) => a + b, 0);
}
Rust/Wasm版
#[wasm_bindgen]
pub fn sum_array_wasm(arr: &[i32]) -> i64 {
arr.iter().map(|&x| x as i64).sum()
}
ベンチマーク結果(100万要素)
| 実装 | 時間 |
|---|---|
| JavaScript | 約5ms |
| Wasm | 約2ms |
配列のコピーオーバーヘッドがあるので、小さい配列だとJSの方が速いことも。
いつWasmを使うべきか
⭕ Wasmが有利
- CPU負荷の高い計算(画像処理、暗号化、物理シミュレーション)
- 大量データの処理
- ゲームのロジック
❌ JSで十分
- DOM操作
- 小さな計算
- I/O待ちが多い処理
実践的な使い方
web-sys で DOM 操作
[dependencies]
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["Document", "Element", "HtmlElement", "Window"] }
use wasm_bindgen::prelude::*;
use web_sys::{Document, Element, Window};
fn window() -> Window {
web_sys::window().expect("no window")
}
fn document() -> Document {
window().document().expect("no document")
}
#[wasm_bindgen]
pub fn create_element(tag: &str, content: &str) -> Result<Element, JsValue> {
let document = document();
let element = document.create_element(tag)?;
element.set_text_content(Some(content));
Ok(element)
}
画像処理の例
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn grayscale(data: &mut [u8]) {
// RGBAフォーマットを想定
for chunk in data.chunks_exact_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] (alpha) はそのまま
}
}
JavaScript側:
import init, { grayscale } from './pkg/wasm_example.js';
async function processImage(canvas) {
await init();
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
grayscale(imageData.data); // Wasmで処理
ctx.putImageData(imageData, 0, 0);
}
wasm-pack のビルドターゲット
# Web (ES Modules)
wasm-pack build --target web
# bundler (webpack等)
wasm-pack build --target bundler
# Node.js
wasm-pack build --target nodejs
デバッグ方法
console_error_panic_hook
パニック時にスタックトレースを表示:
[dependencies]
console_error_panic_hook = "0.1"
use wasm_bindgen::prelude::*;
#[wasm_bindgen(start)]
pub fn init() {
console_error_panic_hook::set_once();
}
wasm-bindgen-test
[dev-dependencies]
wasm-bindgen-test = "0.3"
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}
wasm-pack test --headless --chrome
まとめ
Wasm化の手順
-
wasm-packをインストール -
Cargo.tomlにwasm-bindgenを追加 -
#[wasm_bindgen]で関数を公開 -
wasm-pack build --target webでビルド - JavaScriptからインポートして使用
チェックリスト
- 計算負荷の高い処理をWasm化
-
console_error_panic_hookでデバッグを楽に - 小さい処理はJSのまま(オーバーヘッドを考慮)
-
opt-level = "s"でサイズ最適化
今すぐできるアクション
-
wasm-packをインストール - サンプルプロジェクトを動かしてみる
- 既存の重い処理をWasm化してベンチマーク
RustでWasm、めちゃくちゃ楽しいです。「ブラウザでRustが動く」という体験、ぜひ試してみてください。
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!