一般社団法人 全国個人事業主支援協会

COLUMN コラム

  • WebAssemblyの実用入門:ブラウザで動く高速アプリケーション開発

WebAssemblyが解決する課題

JavaScriptはWebの共通言語として不動の地位を築いているが、CPUヘビーな処理ではパフォーマンスの壁に直面する。画像処理、3Dレンダリング、暗号計算、データ圧縮——こうしたワークロードをブラウザ上で高速に実行したい場合、WebAssembly(Wasm)が有力な選択肢となる。

WebAssemblyは、ブラウザ上で動作するバイナリ命令形式だ。CやC++、Rustなどのコンパイル言語からWasmバイナリを生成し、JavaScriptから呼び出して使用する。ネイティブに近い速度で動作し、すべての主要ブラウザでサポートされている。

筆者は過去2年間、業務で画像処理ツールやデータ変換エンジンをWasmで開発してきた。その経験をもとに、実用的な開発の流れを解説する。

Rustを使ったWasm開発環境の構築

Wasmの開発言語としてはRustが最も成熟したエコシステムを提供している。wasm-packとwasm-bindgenにより、RustからWasmへのコンパイルとJavaScriptとの相互運用が容易だ。

# Rustのインストール(未導入の場合)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# wasm-packのインストール
cargo install wasm-pack

# プロジェクトの作成
cargo new --lib wasm-image-processor
cd wasm-image-processor

Cargo.tomlにWasm用の設定を追加する。

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

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

実践:画像のグレースケール変換

具体例として、画像のピクセルデータをグレースケールに変換する処理をWasmで実装する。この種のピクセル単位の処理は、JavaScriptよりもWasmで5〜10倍高速になることが多い。

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn grayscale(pixels: &mut; [u8]) {
// RGBAの4バイトずつ処理
for chunk in pixels.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) はそのまま
}
}

// ビルド
// wasm-pack build --target web --release

JavaScript側からの呼び出しは以下のようになる。

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

async function processImage(imageData) {
await init();

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.drawImage(imageData, 0, 0);

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

// Wasmでグレースケール変換
const start = performance.now();
grayscale(imgData.data);
const elapsed = performance.now() - start;
console.log(\`Wasm処理時間: \${elapsed.toFixed(2)}ms\`);

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

メモリ管理とデータ受け渡し

WasmとJavaScriptの間のデータ受け渡しは、パフォーマンスのボトルネックになりやすい。Wasmは独自のリニアメモリ空間を持っており、JavaScriptとのデータ交換にはコピーが発生する場合がある。

大量のデータを扱う場合は、Wasmのメモリを直接参照する方法が効率的だ。

use wasm_bindgen::prelude::*;
use std::alloc::{alloc, dealloc, Layout};

#[wasm_bindgen]
pub fn alloc_buffer(size: usize) -> *mut u8 {
let layout = Layout::from_size_align(size, 1).unwrap();
unsafe { alloc(layout) }
}

#[wasm_bindgen]
pub fn free_buffer(ptr: *mut u8, size: usize) {
let layout = Layout::from_size_align(size, 1).unwrap();
unsafe { dealloc(ptr, layout) }
}

#[wasm_bindgen]
pub fn process_buffer(ptr: *mut u8, len: usize) {
let data = unsafe { std::slice::from_raw_parts_mut(ptr, len) };
// data に対して処理を行う
grayscale(data);
}

// JavaScript側:Wasmメモリに直接書き込み
const ptr = alloc_buffer(imageBytes.length);
const wasmMemory = new Uint8Array(memory.buffer, ptr, imageBytes.length);
wasmMemory.set(imageBytes);

process_buffer(ptr, imageBytes.length);

// 結果を読み出し
const result = new Uint8Array(memory.buffer, ptr, imageBytes.length);

// メモリ解放
free_buffer(ptr, imageBytes.length);

ただし、wasm-bindgenが提供する高レベルAPIを使えば、多くのケースでは手動メモリ管理は不要だ。スライス引数として渡せば、wasm-bindgenが自動的にコピーを処理してくれる。手動管理はパフォーマンスがクリティカルな場合にのみ検討すべきだ。

バンドルサイズの最適化

Wasmバイナリのサイズは初期ロード時間に直結する。以下の最適化手法でサイズを大幅に削減できる。

# Cargo.toml に追加
[profile.release]
opt-level = 'z' # サイズ最適化
lto = true # リンク時最適化
codegen-units = 1 # コンパイル単位を1に
panic = 'abort' # パニック時の巻き戻しコードを削除
strip = true # デバッグ情報を削除

さらに、wasm-optツールを使った後処理も効果的だ。筆者のプロジェクトでは、これらの最適化により、初期サイズ800KBのバイナリが120KBまで縮小された。gzip圧縮後は50KB程度になり、十分に実用的なサイズだ。

実運用での注意点

Wasmを本番環境で使う際のポイントをまとめる。

  • 初期化コスト:Wasmモジュールのコンパイルとインスタンス化には時間がかかる。アプリ起動時に非同期で初期化しておくのが望ましい
  • スレッド:SharedArrayBufferとWeb Workersを組み合わせることで、Wasmでマルチスレッド処理が可能。ただしCOOP/COEPヘッダの設定が必要
  • デバッグ:Chrome DevToolsはWasmのデバッグをサポートしているが、ソースマップの品質はまだ発展途上。ログ出力を活用した古典的なデバッグ手法も併用すべきだ
  • フォールバック:万が一Wasmが動作しない環境に備えて、JavaScript実装をフォールバックとして用意しておくと安心だ

まとめ

WebAssemblyは、ブラウザ上でのCPUヘビーな処理を高速化する実用的な技術だ。Rustとwasm-packの組み合わせにより、開発体験も大きく向上している。すべてのWeb処理をWasmにする必要はないが、パフォーマンスがボトルネックとなる箇所にピンポイントで適用することで、ユーザー体験を劇的に改善できる。まずは小さな処理から試してみることを勧める。

この記事をシェアする

  • Twitterでシェア
  • Facebookでシェア
  • LINEでシェア