15
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

「JavaScriptが遅い」という問題を解決するために登場したWebAssembly(Wasm)。

RustはWasm対応が優秀で、めっちゃ簡単にブラウザで動くバイナリが作れます。

実際に作ってみたらマジで速かったので、その方法を紹介します。

目次

  1. WebAssemblyとは
  2. 環境構築
  3. Hello Wasm
  4. JavaScriptとの連携
  5. パフォーマンス比較
  6. 実践的な使い方
  7. まとめ

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化の手順

  1. wasm-pack をインストール
  2. Cargo.tomlwasm-bindgen を追加
  3. #[wasm_bindgen] で関数を公開
  4. wasm-pack build --target web でビルド
  5. JavaScriptからインポートして使用

チェックリスト

  • 計算負荷の高い処理をWasm化
  • console_error_panic_hook でデバッグを楽に
  • 小さい処理はJSのまま(オーバーヘッドを考慮)
  • opt-level = "s" でサイズ最適化

今すぐできるアクション

  1. wasm-pack をインストール
  2. サンプルプロジェクトを動かしてみる
  3. 既存の重い処理をWasm化してベンチマーク

RustでWasm、めちゃくちゃ楽しいです。「ブラウザでRustが動く」という体験、ぜひ試してみてください。

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

15
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?