Rust + WebAssemblyでフロントエンド高速化 — 実践入門ガイド


なぜRust + WebAssembly?

JavaScriptは柔軟ですが、計算量の多い処理では性能がボトルネックになります。WebAssembly (WASM) を使えば、Rustのような低レベル言語で書いたコードをブラウザで実行でき、大幅な高速化が可能です。

パフォーマンス比較

処理JavaScriptRust + WASM高速化率
画像フィルタ処理850ms45ms19倍
SHA-256ハッシュ120ms8ms15倍
大量データソート320ms25ms13倍

セットアップ

必要なツール

# Rustインストール
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# wasm-packインストール
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

# プロジェクト作成テンプレート
cargo install cargo-generate

プロジェクト作成

# Rustプロジェクト作成
cargo new --lib my-wasm-project
cd my-wasm-project

Cargo.toml を編集:

[package]
name = "my-wasm-project"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = ["console"] }

[profile.release]
opt-level = "z"     # サイズ最適化
lto = true          # Link Time Optimization
codegen-units = 1   # 並列化を無効にして最適化優先

Hello World

Rustコード

// src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

// JavaScriptのconsole.logを呼ぶ
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

#[wasm_bindgen]
pub fn say_hello() {
    log("Hello from Rust!");
}

ビルド

# WASMビルド
wasm-pack build --target web

# サイズ確認
ls -lh pkg/

JavaScriptから使用

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Rust + WASM</title>
</head>
<body>
    <script type="module">
        import init, { greet, add, say_hello } from './pkg/my_wasm_project.js';

        async function run() {
            await init(); // WASMを初期化

            console.log(greet("World")); // "Hello, World!"
            console.log(add(10, 20));    // 30
            say_hello();                 // コンソールに出力
        }

        run();
    </script>
</body>
</html>

実践例1: 画像処理(グレースケール変換)

Rustコード

// src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn grayscale(data: &mut [u8]) {
    for pixel in data.chunks_exact_mut(4) {
        let r = pixel[0] as f32;
        let g = pixel[1] as f32;
        let b = pixel[2] as f32;

        // 輝度計算
        let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;

        pixel[0] = gray;
        pixel[1] = gray;
        pixel[2] = gray;
        // pixel[3] (alpha) はそのまま
    }
}

#[wasm_bindgen]
pub fn sepia(data: &mut [u8]) {
    for pixel in data.chunks_exact_mut(4) {
        let r = pixel[0] as f32;
        let g = pixel[1] as f32;
        let b = pixel[2] as f32;

        pixel[0] = ((r * 0.393) + (g * 0.769) + (b * 0.189)).min(255.0) as u8;
        pixel[1] = ((r * 0.349) + (g * 0.686) + (b * 0.168)).min(255.0) as u8;
        pixel[2] = ((r * 0.272) + (g * 0.534) + (b * 0.131)).min(255.0) as u8;
    }
}

JavaScriptから呼び出し

import init, { grayscale, sepia } from './pkg/my_wasm_project.js';

await init();

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();

img.onload = () => {
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0);

    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    // Rust関数を呼び出し
    console.time('grayscale');
    grayscale(imageData.data);
    console.timeEnd('grayscale'); // 通常、JavaScriptの10-20倍高速

    ctx.putImageData(imageData, 0, 0);
};

img.src = 'photo.jpg';

実践例2: SHA-256ハッシュ計算

// Cargo.toml に追加
// [dependencies]
// sha2 = "0.10"

use wasm_bindgen::prelude::*;
use sha2::{Sha256, Digest};

#[wasm_bindgen]
pub fn sha256(input: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(input.as_bytes());
    let result = hasher.finalize();

    // 16進数文字列に変換
    result.iter()
        .map(|b| format!("{:02x}", b))
        .collect::<String>()
}

#[wasm_bindgen]
pub fn hash_file(data: &[u8]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(data);
    let result = hasher.finalize();

    result.iter()
        .map(|b| format!("{:02x}", b))
        .collect::<String>()
}
import init, { sha256, hash_file } from './pkg/my_wasm_project.js';

await init();

// テキストのハッシュ
console.log(sha256("Hello, World!"));
// 出力: dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f

// ファイルのハッシュ
document.getElementById('file-input').addEventListener('change', async (e) => {
    const file = e.target.files[0];
    const buffer = await file.arrayBuffer();
    const uint8 = new Uint8Array(buffer);

    console.time('hash');
    const hash = hash_file(uint8);
    console.timeEnd('hash');

    console.log('SHA-256:', hash);
});

実践例3: データ圧縮(LZ4)

// Cargo.toml
// [dependencies]
// lz4_flex = "0.11"

use wasm_bindgen::prelude::*;
use lz4_flex::{compress_prepend_size, decompress_size_prepended};

#[wasm_bindgen]
pub fn compress(data: &[u8]) -> Vec<u8> {
    compress_prepend_size(data)
}

#[wasm_bindgen]
pub fn decompress(data: &[u8]) -> Result<Vec<u8>, JsValue> {
    decompress_size_prepended(data)
        .map_err(|e| JsValue::from_str(&e.to_string()))
}

#[wasm_bindgen]
pub fn compression_ratio(original_size: usize, compressed_size: usize) -> f64 {
    (1.0 - (compressed_size as f64 / original_size as f64)) * 100.0
}
import init, { compress, decompress, compression_ratio } from './pkg/my_wasm_project.js';

await init();

const text = "Lorem ipsum dolor sit amet...".repeat(100);
const encoder = new TextEncoder();
const data = encoder.encode(text);

console.log('Original size:', data.length);

const compressed = compress(data);
console.log('Compressed size:', compressed.length);
console.log('Ratio:', compression_ratio(data.length, compressed.length).toFixed(2) + '%');

const decompressed = decompress(compressed);
const decoder = new TextDecoder();
console.log('Decompressed:', decoder.decode(decompressed));

JavaScript連携(高度)

Rust側でJavaScriptの関数を呼ぶ

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    // JavaScriptのalert関数
    fn alert(s: &str);

    // カスタム関数
    #[wasm_bindgen(js_namespace = myApp)]
    fn onProgress(percent: f64);
}

#[wasm_bindgen]
pub fn heavy_computation() {
    for i in 0..100 {
        // 重い処理...

        // 進捗をJavaScriptに通知
        onProgress(i as f64);
    }
    alert("Complete!");
}
window.myApp = {
    onProgress: (percent) => {
        console.log(`Progress: ${percent}%`);
        document.getElementById('progress').value = percent;
    }
};

JavaScriptのオブジェクトをRustで扱う

use wasm_bindgen::prelude::*;
use js_sys::{Array, Object, Reflect};

#[wasm_bindgen]
pub fn process_user(user: &JsValue) -> Result<String, JsValue> {
    let name = Reflect::get(user, &"name".into())?
        .as_string()
        .unwrap_or_default();

    let age = Reflect::get(user, &"age".into())?
        .as_f64()
        .unwrap_or(0.0) as u32;

    Ok(format!("{} is {} years old", name, age))
}
const user = { name: "Alice", age: 30 };
console.log(process_user(user)); // "Alice is 30 years old"

パフォーマンス最適化

1. ビルド最適化

# リリースビルド(最適化有効)
wasm-pack build --release --target web

# さらに最適化(wasm-opt使用)
npm install -g wasm-opt
wasm-opt -Oz -o output.wasm input.wasm

2. メモリ管理

// 大きなデータはJavaScriptに返さず、ポインタで渡す
#[wasm_bindgen]
pub struct ImageBuffer {
    data: Vec<u8>,
}

#[wasm_bindgen]
impl ImageBuffer {
    #[wasm_bindgen(constructor)]
    pub fn new(width: u32, height: u32) -> Self {
        Self {
            data: vec![0; (width * height * 4) as usize],
        }
    }

    pub fn get_ptr(&self) -> *const u8 {
        self.data.as_ptr()
    }

    pub fn len(&self) -> usize {
        self.data.len()
    }
}

Next.js/Viteでの統合

Next.js

npm install @wasm-tool/wasm-pack-plugin
// next.config.js
const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin');

module.exports = {
    webpack: (config, { isServer }) => {
        if (!isServer) {
            config.plugins.push(
                new WasmPackPlugin({
                    crateDirectory: require('path').resolve(__dirname, '../my-wasm-project'),
                })
            );
        }
        return config;
    },
};

Vite

npm install vite-plugin-wasm
// vite.config.js
import wasm from 'vite-plugin-wasm';

export default {
    plugins: [wasm()],
};

まとめ

Rust + WebAssemblyは以下のような場面で威力を発揮します:

  • 画像/動画処理
  • 暗号化・ハッシュ計算
  • データ圧縮・解凍
  • ゲームエンジン
  • 科学計算・シミュレーション

JavaScriptの柔軟性とRustの性能を組み合わせることで、次世代のWebアプリケーションを構築できます。

参考リンク: