【2026年版】Rust + WebAssemblyでブラウザAI推論を実現する:実装ガイドとパフォーマンス検証

Tech Trends AI
- 7 minutes read - 1472 wordsはじめに:ブラウザでAI推論を行う意義
2026年、AIモデルの推論はクラウドだけでなく**ブラウザ上(クライアントサイド)**で行うユースケースが急速に広がっています。ブラウザAI推論には以下の利点があります。
- プライバシー保護: ユーザーデータがサーバーに送信されない
- レイテンシ削減: ネットワーク往復が不要で即座にレスポンスを返せる
- オフライン動作: ネットワーク接続なしでAI機能を提供
- サーバーコスト削減: GPU推論インフラが不要
この実現に最適な技術スタックが**Rust + WebAssembly(WASM)**の組み合わせです。Rustのメモリ安全性とゼロコスト抽象化、WASMのポータビリティとニアネイティブ性能を活かすことで、ブラウザ上でも実用的なAI推論パイプラインを構築できます。
本記事では、Rust + WASMによるブラウザAI推論の実装方法を、環境構築からパフォーマンス最適化まで体系的に解説します。
技術スタックの全体像
アーキテクチャ概要
[ブラウザ]
├── JavaScript / TypeScript(UI層)
├── Web Workers(推論スレッド)
│ └── WASM Module(Rustコンパイル)
│ ├── ONNX Runtime(推論エンジン)
│ ├── 前処理ロジック
│ └── 後処理ロジック
└── WebGPU / WebGL(GPU アクセラレーション)
主要技術コンポーネント
| コンポーネント | 役割 | 2026年の状況 |
|---|---|---|
| Rust | WASMモジュールの記述言語 | 2024 Edition安定版 |
| wasm-bindgen | Rust ↔ JS間のバインディング | v0.2.95+ |
| wasm-pack | ビルドツールチェーン | v0.13+ |
| ONNX Runtime Web | AI推論エンジン | v1.19+、WASM backend |
| WebGPU | GPU アクセラレーション | 主要ブラウザで安定版 |
| Web Workers | メインスレッド非ブロック | 標準仕様 |
競合技術との比較
| 技術 | 言語 | 性能 | メモリ管理 | エコシステム | ブラウザAI適性 |
|---|---|---|---|---|---|
| Rust + WASM | Rust | 最高 | 安全・効率的 | 成長中 | 最適 |
| C/C++ + WASM | C/C++ | 最高 | 手動 | 成熟 | 良好 |
| TensorFlow.js | JavaScript | 中 | GC依存 | 充実 | 良好 |
| ONNX Runtime Web | JS/TS | 高 | 管理型 | 充実 | 良好 |
| Python + Pyodide | Python | 低 | GC依存 | 豊富 | 限定的 |
開発環境のセットアップ
必要なツールのインストール
# Rustのインストール(rustup経由)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
# WASMターゲットの追加
rustup target add wasm32-unknown-unknown
# wasm-packのインストール
cargo install wasm-pack
# wasm-opt(最適化ツール)のインストール
cargo install wasm-opt
# プロジェクトの作成
cargo new --lib browser-ai-inference
cd browser-ai-inference
Cargo.tomlの設定
[package]
name = "browser-ai-inference"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2.95"
js-sys = "0.3.72"
web-sys = { version = "0.3.72", features = [
"console",
"Window",
"Performance",
"ImageData",
"HtmlCanvasElement",
"CanvasRenderingContext2d",
] }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"
ndarray = "0.16"
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
# ONNX Runtime
ort = { version = "2.0", default-features = false, features = ["wasm"] }
[profile.release]
opt-level = "z" # サイズ最適化
lto = true # リンク時最適化
codegen-units = 1 # コード生成ユニットを1に
strip = true # デバッグ情報を削除
画像分類モデルの実装例
Rustモジュールの実装
// src/lib.rs
use wasm_bindgen::prelude::*;
use ndarray::{Array, Array4, s};
use serde::{Deserialize, Serialize};
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
macro_rules! console_log {
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}
#[derive(Serialize, Deserialize)]
pub struct ClassificationResult {
pub label: String,
pub confidence: f32,
}
#[derive(Serialize, Deserialize)]
pub struct InferenceOutput {
pub results: Vec<ClassificationResult>,
pub inference_time_ms: f64,
}
/// 画像の前処理:リサイズ、正規化、テンソル変換
#[wasm_bindgen]
pub fn preprocess_image(
image_data: &[u8],
width: u32,
height: u32,
target_size: u32,
) -> Vec<f32> {
let target = target_size as usize;
// バイリニア補間によるリサイズ
let mut resized = vec![0.0f32; target * target * 3];
let scale_x = width as f32 / target as f32;
let scale_y = height as f32 / target as f32;
for y in 0..target {
for x in 0..target {
let src_x = x as f32 * scale_x;
let src_y = y as f32 * scale_y;
let x0 = src_x as u32;
let y0 = src_y as u32;
let x1 = (x0 + 1).min(width - 1);
let y1 = (y0 + 1).min(height - 1);
let fx = src_x - x0 as f32;
let fy = src_y - y0 as f32;
for c in 0..3 {
let idx00 = ((y0 * width + x0) * 4 + c) as usize;
let idx01 = ((y0 * width + x1) * 4 + c) as usize;
let idx10 = ((y1 * width + x0) * 4 + c) as usize;
let idx11 = ((y1 * width + x1) * 4 + c) as usize;
let v00 = image_data[idx00] as f32;
let v01 = image_data[idx01] as f32;
let v10 = image_data[idx10] as f32;
let v11 = image_data[idx11] as f32;
let value = v00 * (1.0 - fx) * (1.0 - fy)
+ v01 * fx * (1.0 - fy)
+ v10 * (1.0 - fx) * fy
+ v11 * fx * fy;
// ImageNet正規化(mean, std)
let normalized = match c {
0 => (value / 255.0 - 0.485) / 0.229, // R
1 => (value / 255.0 - 0.456) / 0.224, // G
2 => (value / 255.0 - 0.406) / 0.225, // B
_ => unreachable!(),
};
// NCHW形式で格納
let out_idx = c as usize * target * target + y * target + x;
resized[out_idx] = normalized;
}
}
}
resized
}
/// Softmax関数の実装
fn softmax(logits: &[f32]) -> Vec<f32> {
let max_val = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let exp_sum: f32 = logits.iter().map(|x| (x - max_val).exp()).sum();
logits.iter().map(|x| (x - max_val).exp() / exp_sum).collect()
}
/// Top-K結果の取得
#[wasm_bindgen]
pub fn get_top_k(probabilities: &[f32], labels: JsValue, k: usize) -> JsValue {
let labels: Vec<String> = serde_wasm_bindgen::from_value(labels).unwrap();
let mut indexed: Vec<(usize, f32)> = probabilities
.iter()
.enumerate()
.map(|(i, &p)| (i, p))
.collect();
indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let results: Vec<ClassificationResult> = indexed
.into_iter()
.take(k)
.map(|(idx, conf)| ClassificationResult {
label: labels.get(idx).cloned().unwrap_or_default(),
confidence: conf,
})
.collect();
serde_wasm_bindgen::to_value(&results).unwrap()
}
JavaScriptからの呼び出し
// index.js
import init, {
preprocess_image,
get_top_k
} from './pkg/browser_ai_inference.js';
// ONNX Runtime Webの初期化
import * as ort from 'onnxruntime-web';
class BrowserAIInference {
constructor() {
this.session = null;
this.labels = null;
}
async initialize(modelPath, labelsPath) {
// WASMモジュールの初期化
await init();
// ONNX Runtimeセッションの作成
ort.env.wasm.numThreads = navigator.hardwareConcurrency || 4;
ort.env.wasm.simd = true;
this.session = await ort.InferenceSession.create(modelPath, {
executionProviders: ['wasm'], // または 'webgpu'
graphOptimizationLevel: 'all',
});
// ラベルの読み込み
const response = await fetch(labelsPath);
this.labels = await response.json();
console.log('Model loaded successfully');
}
async classify(imageElement) {
const startTime = performance.now();
// Canvas経由で画像データを取得
const canvas = document.createElement('canvas');
canvas.width = imageElement.naturalWidth;
canvas.height = imageElement.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(imageElement, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Rustモジュールで前処理
const preprocessed = preprocess_image(
new Uint8Array(imageData.data.buffer),
canvas.width,
canvas.height,
224 // MobileNetのインプットサイズ
);
// ONNX推論の実行
const inputTensor = new ort.Tensor('float32', preprocessed, [1, 3, 224, 224]);
const results = await this.session.run({ input: inputTensor });
const outputData = results.output.data;
// Rustモジュールで後処理(Top-5)
const topResults = get_top_k(
new Float32Array(outputData),
this.labels,
5
);
const inferenceTime = performance.now() - startTime;
return {
results: topResults,
inferenceTimeMs: inferenceTime
};
}
}
// 使用例
const ai = new BrowserAIInference();
await ai.initialize('/models/mobilenet_v3.onnx', '/models/imagenet_labels.json');
const img = document.getElementById('target-image');
const result = await ai.classify(img);
console.log('Classification result:', result);
Web Workers による非同期推論
Worker内での推論実行
メインスレッドのブロックを防ぐため、推論処理はWeb Worker内で実行します。
// inference-worker.js
import init, { preprocess_image, get_top_k } from './pkg/browser_ai_inference.js';
import * as ort from 'onnxruntime-web';
let session = null;
let labels = null;
self.onmessage = async (event) => {
const { type, payload } = event.data;
switch (type) {
case 'init':
await init();
ort.env.wasm.numThreads = 4;
session = await ort.InferenceSession.create(payload.modelUrl, {
executionProviders: ['wasm'],
});
const resp = await fetch(payload.labelsUrl);
labels = await resp.json();
self.postMessage({ type: 'ready' });
break;
case 'infer':
const start = performance.now();
const preprocessed = preprocess_image(
new Uint8Array(payload.imageData),
payload.width,
payload.height,
224
);
const tensor = new ort.Tensor('float32', preprocessed, [1, 3, 224, 224]);
const results = await session.run({ input: tensor });
const topK = get_top_k(new Float32Array(results.output.data), labels, 5);
const elapsed = performance.now() - start;
self.postMessage({
type: 'result',
payload: { results: topK, inferenceTimeMs: elapsed }
});
break;
}
};
// メインスレッド側
const worker = new Worker(new URL('./inference-worker.js', import.meta.url), {
type: 'module'
});
worker.postMessage({
type: 'init',
payload: {
modelUrl: '/models/mobilenet_v3.onnx',
labelsUrl: '/models/imagenet_labels.json'
}
});
worker.onmessage = (event) => {
if (event.data.type === 'result') {
updateUI(event.data.payload);
}
};
WebGPUアクセラレーション
WebGPUバックエンドの利用
2026年現在、主要ブラウザ(Chrome、Edge、Firefox)でWebGPUが安定版として利用可能です。
// WebGPU対応チェックとフォールバック
async function createSession(modelPath) {
const providers = [];
// WebGPUの利用可能性をチェック
if ('gpu' in navigator) {
const adapter = await navigator.gpu.requestAdapter();
if (adapter) {
providers.push('webgpu');
console.log('WebGPU backend enabled');
}
}
// フォールバック
providers.push('wasm');
return await ort.InferenceSession.create(modelPath, {
executionProviders: providers,
graphOptimizationLevel: 'all',
});
}
バックエンド別パフォーマンス比較
| モデル | WASM(CPU) | WebGL | WebGPU | ネイティブGPU |
|---|---|---|---|---|
| MobileNet v3 | 45ms | 12ms | 5ms | 2ms |
| ResNet-50 | 280ms | 85ms | 25ms | 8ms |
| BERT-base | 520ms | - | 95ms | 30ms |
| Whisper-tiny | 1,200ms | - | 280ms | 80ms |
| YOLOv8-nano | 180ms | 55ms | 18ms | 6ms |
モデルの最適化と配信
ONNXモデルの最適化
# モデルの変換と最適化(Python側で事前に実行)
import onnx
from onnxruntime.quantization import quantize_dynamic, QuantType
# PyTorchモデルからONNXへの変換
import torch
import torchvision.models as models
model = models.mobilenet_v3_small(pretrained=True)
model.eval()
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model,
dummy_input,
"mobilenet_v3.onnx",
opset_version=17,
input_names=["input"],
output_names=["output"],
dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}
)
# INT8量子化でモデルサイズを削減
quantize_dynamic(
"mobilenet_v3.onnx",
"mobilenet_v3_int8.onnx",
weight_type=QuantType.QUInt8
)
モデルサイズの最適化比較
| モデル | FP32サイズ | INT8サイズ | 削減率 | 精度低下 |
|---|---|---|---|---|
| MobileNet v3 Small | 9.8MB | 2.7MB | 72% | < 0.5% |
| MobileNet v3 Large | 21.1MB | 5.5MB | 74% | < 0.8% |
| EfficientNet-B0 | 20.4MB | 5.3MB | 74% | < 0.5% |
| BERT-base | 438MB | 113MB | 74% | < 1.0% |
| Whisper-tiny | 151MB | 39MB | 74% | < 1.5% |
効率的なモデル配信
// モデルのキャッシュ戦略
class ModelCache {
constructor(cacheName = 'ai-models-v1') {
this.cacheName = cacheName;
}
async getModel(url) {
const cache = await caches.open(this.cacheName);
let response = await cache.match(url);
if (!response) {
console.log('Downloading model...');
response = await fetch(url);
await cache.put(url, response.clone());
} else {
console.log('Loading model from cache');
}
return await response.arrayBuffer();
}
async clearCache() {
await caches.delete(this.cacheName);
}
}
// Service Workerによるプリキャッシュ
// sw.js
const MODELS_TO_CACHE = [
'/models/mobilenet_v3_int8.onnx',
'/models/imagenet_labels.json',
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('ai-models-v1').then((cache) => {
return cache.addAll(MODELS_TO_CACHE);
})
);
});
WASMバイナリの最適化
ビルドとサイズ最適化
# リリースビルド
wasm-pack build --target web --release
# wasm-optによる追加最適化
wasm-opt -Oz -o pkg/browser_ai_inference_bg_opt.wasm pkg/browser_ai_inference_bg.wasm
# サイズ確認
ls -lh pkg/*.wasm
最適化テクニック比較
| テクニック | サイズ削減 | 性能影響 | 適用方法 |
|---|---|---|---|
| opt-level = “z” | 20〜30% | 微減 | Cargo.toml |
| LTO(リンク時最適化) | 10〜20% | 向上 | Cargo.toml |
| wasm-opt -Oz | 5〜15% | 微減 | ビルド後処理 |
| strip | 10〜15% | なし | Cargo.toml |
| gzip圧縮 | 60〜70% | なし(転送時) | サーバー設定 |
| brotli圧縮 | 65〜75% | なし(転送時) | サーバー設定 |
実用的なユースケース
ブラウザAI推論の適用領域
| ユースケース | モデル | 推論時間目標 | 実現可能性 |
|---|---|---|---|
| 画像分類 | MobileNet v3 | < 50ms | 実用的 |
| 物体検出 | YOLOv8-nano | < 100ms | 実用的 |
| テキスト分類 | DistilBERT | < 200ms | 実用的 |
| 音声認識 | Whisper-tiny | < 1s | 実用的 |
| 顔検出 | BlazeFace | < 30ms | 実用的 |
| 文章生成(小型LLM) | Phi-3-mini | 1〜5s | 限定的 |
| 画像生成 | Stable Diffusion | 30〜60s | 実験的 |
まとめ
Rust + WebAssemblyの組み合わせは、ブラウザ上のAI推論を実現する最も効率的な技術スタックです。本記事の要点をまとめます。
- Rust + WASMの利点: メモリ安全性、ゼロコスト抽象化、ニアネイティブ性能をブラウザで実現
- ONNX Runtimeの活用: PyTorch/TensorFlowモデルをONNX形式に変換し、ブラウザで推論可能
- WebGPUアクセラレーション: GPU対応により、WASM CPU比で5〜10倍の高速化を実現
- Web Workers: メインスレッドを非ブロックにし、スムーズなUI体験を維持
- モデル最適化: INT8量子化とWASMバイナリ最適化で、モバイルデバイスでも実用的な速度を達成
- キャッシュ戦略: Service WorkerとCache APIで、2回目以降の読み込みを高速化
ブラウザAI推論は、プライバシー重視のアプリケーション、オフライン対応、リアルタイム処理が求められるシーンで特に有効です。今後WebGPUの普及とWASMの機能拡張(GC、SIMD、スレッド)により、さらに高度なモデルのブラウザ推論が可能になるでしょう。