ブラウザの外に飛び出したWebAssembly:WASI 2.0、コンポーネントモデル、そしてインフラの未来
Web開発者のほとんどにとって、WebAssemblyと言えば「ブラウザで重い処理を高速化するもの」ですよね。FigmaとかWeb版Photoshopとか、ブラウザだけで動く動画編集ツールとか。「すごいけど自分には関係ないかな」くらいの感覚だったと思います。
でも最近、ちょっと状況が変わってきています。WebAssemblyがブラウザの外に出てきたんです。サーバー、エッジ関数、IoTデバイス、さらにDockerコンテナの代替としても使われ始めています。
WASI 0.3.0が今月リリースされました。コンポーネントモデルの安定化が進んでいます。Cloudflare WorkersやFastlyは本番環境でWasmを動かしていて、DockerもWasmのネイティブサポートを追加しました。
先の話じゃないです。今すぐ試せます。
そもそもWASIって何?
一言でいうと
WASI(WebAssembly System Interface)は、Wasmモジュールがファイル、ネットワーク、時計などのシステムリソースにアクセスするための標準インターフェースです。雑に言うと、Wasm版POSIX。
なぜ今話題になっているのか
WASIがないと、WebAssemblyは純粋な計算しかできません。数値計算はOK、でもファイル読み込みに、HTTPリクエストに、現在時刻の取得すらできない。
ブラウザではJavaScriptがI/Oを担当してくれるので問題ありませんが、サーバーサイドではこれでは何もできません。
WASIがこの問題を解決します:
WASIなし: WASIあり:
┌──────────────┐ ┌──────────────┐
│ Wasmモジュール │ │ Wasmモジュール │
│ │ │ │
│ 計算のみ │ │ ファイル │
│ │ │ ネットワーク │
│ I/O不可 │ │ タイマー │
│ ファイル不可 │ │ 環境変数 │
│ ネットワーク不可│ │ HTTPリクエスト│
└──────────────┘ └──────────────┘
セキュリティがデフォルトという発想
ここがWASIの面白いところ。普通のOSプロセスってデフォルトで何でもできますよね。WASIは逆。明示的に許可されたものだけアクセスできるんです:
# wasmtimeでWasmモジュールを実行 # このモジュールは /data の読み取りと /output への書き込みのみ可能 wasmtime run --dir /data::readonly --dir /output myapp.wasm # このモジュールができないこと: # ❌ /etc/passwd へのアクセス # ❌ ネットワークリクエスト(--tcplistenが未指定) # ❌ 環境変数の読み取り(--envが未指定) # ❌ 許可されていないディレクトリへのアクセス
Dockerコンテナはデフォルトで自分のファイルシステム全体にアクセスできますよね。WASIは最初からロックされています。セキュリティが設定ではなくデフォルト値というわけです。
WASI 0.3.0で何が変わったか
非同期I/Oサポート
今回一番大きい変更。以前のWASIはブロッキングI/Oのみで、複数の処理を同時にさばくのが難しかった。それが0.3.0でfuturesとstreamsモデルが入りました:
// WASI 0.3.0 非同期HTTPハンドラー use wasi::http::incoming_handler; use wasi::io::streams; async fn handle_request(request: IncomingRequest) -> OutgoingResponse { // ノンブロッキングのファイル読み込み let data = streams::read("config.json").await?; // ノンブロッキングのHTTPリクエスト let api_response = wasi::http::outgoing_handler::handle( OutgoingRequest::new("https://api.example.com/data") ).await?; // レスポンス生成 OutgoingResponse::new(200, api_response.body()) }
これがないと、サーバー用途でWasmを使う理由がなかった。ようやくNode.jsやGoと勝負できる土俯が整ったわけです。
安定化されたインターフェース一覧
| インターフェース | 機能 | ステータス |
|---|---|---|
wasi:filesystem | ファイル・ディレクトリの読み書き | 安定版 |
wasi:sockets | TCP/UDPネットワーキング | 安定版 |
wasi:http | HTTPクライアント・サーバー | 安定版 |
wasi:clocks | 時計・タイマー | 安定版 |
wasi:random | 暗号論的乱数 | 安定版 |
wasi:cli | 引数、環境変数、stdio | 安定版 |
wasi:io | 非同期ストリーム・フューチャー | 0.3で新規追加 |
まだできないこと(正直に)
- GPUアクセス: 標準化されたGPUインターフェースはまだありません。AIトレーニングやグラフィックス処理は対象外です。
- スレッディング: WASIスレッドの提案はありますが0.3.0には未実装。非同期の並行処理はできますが、真の並列処理はまだです。
- DOMアクセス: WASIはブラウザ外の環境向けです。ブラウザのWasmは引き続きJavaScript経由でDOMにアクセスします。
本当のゲームチェンジャーはコンポーネントモデルのほう
WASIがWasmにI/O能力を与えたのに対し、コンポーネントモデルが与えるのは組み合わせる力。ここがヤバい。
今何が困っているのか
Rustで書かれた画像処理ライブラリをGoアプリで使いたい場合、今の選択肢は:
- Goで書き直す(大変)
- CGoでCバインディング(つらい)
- 別サービスにしてHTTPで呼ぶ(遅い、複雑)
コンポーネントモデルは4つ目の選択肢を提供します:両方をWasmコンポーネントにコンパイルして直接リンク。メモリを共有し、関数を直接呼び出し、同じプロセス内で動きます。言語が違っても。
従来のアプローチ:
┌──────────┐ HTTP ┌──────────┐
│ Goアプリ │ ←────────→ │ Rustサービス│
│ │ ネットワーク │ │
└──────────┘ オーバーヘッド └──────────┘
コンポーネントモデル:
┌────────────────────────────────┐
│ 合成されたWasmコンポーネント │
│ ┌──────────┐ ┌──────────┐ │
│ │ Go │←→│ Rust │ │
│ │ ロジック │ │ ライブラリ │ │
│ └──────────┘ └──────────┘ │
│ 直接関数呼び出し、メモリ共有 │
│ ネットワークオーバーヘッドゼロ │
└────────────────────────────────┘
WIT:コンポーネントの共通言語
コンポーネント同士の通信には**WIT(Wasm Interface Type)**定義を使います。protobufを使ったことがあれば似た感覚です:
// image-processor.wit package mycompany:[email protected]; interface process { record image { width: u32, height: u32, data: list<u8>, } record options { quality: u8, format: string, } resize: func(img: image, target-width: u32, target-height: u32) -> image; compress: func(img: image, opts: options) -> list<u8>; detect-faces: func(img: image) -> list<bounding-box>; record bounding-box { x: u32, y: u32, width: u32, height: u32, } } world image-service { export process; }
このWITを一度定義すれば:
- Rust、Go、Python、JavaScript、C/C++、**C#**のどれでも実装可能
- どの言語からでも呼び出し可能
- FFIなし、シリアライズなし、グルーコードなし
実際に作ってみる
話だけだとピンとこないので、実際にRustでコンポーネントを作ってJavaScriptから呼ぶ例をやってみましょう:
ステップ1: インターフェース定義
// greeter.wit package example:[email protected]; interface greet { greet: func(name: string) -> string; } world greeter { export greet; }
ステップ2: Rustで実装
// src/lib.rs wit_bindgen::generate!("greeter"); struct MyGreeter; impl Guest for MyGreeter { fn greet(name: String) -> String { format!("Hello, {}! Welcome to the Component Model.", name) } } export!(MyGreeter);
# Cargo.toml [package] name = "greeter" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] wit-bindgen = "0.36"
ステップ3: ビルド
# Wasmモジュールをビルド cargo build --target wasm32-wasip2 --release # wasm32-wasip2ターゲットなので既にコンポーネントとしてビルド済み # target/wasm32-wasip2/release/greeter.wasm
ステップ4: JavaScriptから使う(jco利用)
# jco(JavaScript Component Tools)をインストール npm install -g @bytecodealliance/jco # WasmコンポーネントをJSからインポート可能なモジュールに変換 jco transpile greeter.wasm -o greeter-js/
// app.js import { greet } from './greeter-js/greeter.js'; console.log(greet("World")); // "Hello, World! Welcome to the Component Model."
Rustの関数をJavaScriptから直接呼び出し。シリアライズゼロ、HTTP呼び出しゼロ、型安全性も確保。
本番環境でWasmはどこで使われているか
1. エッジコンピューティング:Cloudflare Workers、Fastly Compute
最も成熟した本番ユースケースです。Cloudflare WorkersとFastly Computeの両方がWasmをネイティブ実行しています:
// Cloudflare WorkerでWasmを使用 // 重い処理はWasmで、ルーティングはJSで import { process_image } from './image-processor.wasm'; export default { async fetch(request) { const imageData = await request.arrayBuffer(); // ネイティブに近い速度で実行 const result = process_image( new Uint8Array(imageData), 800, // 目標幅 600 // 目標高さ ); return new Response(result, { headers: { 'Content-Type': 'image/webp' }, }); }, };
なぜエッジでWasmなのか:
- コールドスタート: コンテナの数百ms → Wasmはマイクロ秒
- メモリ: Node.jsの~50-200MB → Wasmの1-10MB
- セキュリティ: デフォルトでサンドボックス化、コンテナ脱出も不可能
- ポータビリティ: 同じバイナリがx86、ARM、RISC-Vどこでも動く
2. プラグインシステム:他人のコードを安全に動かす
急速に広まっているユースケース。ユーザープラグインを同一プロセスで動かすとセキュリティがやばいし、別コンテナだと遅い。Wasmコンポーネントなら両方解決:
従来のプラグインアーキテクチャ:
┌─────────────────────────┐
│ ホストアプリケーション │
│ ┌───────────────────┐ │
│ │ プラグイン(JS/Lua)│ │ ← ホストメモリに完全アクセス
│ │ ホストをクラッシュ可能│ │ ← ホストのファイルシステムにアクセス可能
│ │ メモリリーク可能 │ │ ← 任意のシステムコール可能
│ └───────────────────┘ │
└─────────────────────────┘
Wasmプラグインアーキテクチャ:
┌─────────────────────────┐
│ ホストアプリケーション │
│ ┌───────────────────┐ │
│ │ プラグイン(Wasm) │ │ ← 隔離されたメモリサンドボックス
│ │ ホストクラッシュ不可│ │ ← 許可された機能のみ
│ │ メモリ安全 │ │ ← 決定論的リソース使用
│ └───────────────────┘ │
└─────────────────────────┘
実際の事例:
- Envoy Proxy: カスタムルーティング・フィルタリングをWasmプラグインで
- Shopify Functions: マーチャントのロジックをWasmで実行
- Figma: プラグインがWasmサンドボックスで実行
- Zedエディタ: 拡張機能がWasmコンポーネント
- Extism: Wasmベースのプラグインシステムフレームワーク
3. Docker + Wasm:コンテナの代替
DockerがDocker Desktop 4.15+でWasmのネイティブサポートを追加しました。従来のLinuxコンテナと並行してWasmコンテナを実行できます:
# 従来のDockerfile FROM node:20-slim WORKDIR /app COPY . . RUN npm install CMD ["node", "server.js"] # イメージサイズ: ~200MB # コールドスタート: ~500ms
# Wasm "Dockerfile"(wasm/wasiベース) FROM scratch COPY myapp.wasm /myapp.wasm ENTRYPOINT ["/myapp.wasm"] # イメージサイズ: ~2MB # コールドスタート: ~1ms
# Wasmコンテナを実行 docker run --runtime=io.containerd.wasmtime.v2 \ --platform wasi/wasm \ myregistry/myapp:latest
Wasmコンテナが適切な場面:
- ✅ ステートレスなAPIエンドポイント
- ✅ データ処理パイプライン
- ✅ CLIツール・ユーティリティ
- ✅ 高速コールドスタートが必要な関数
- ❌ GPUが必要なアプリケーション
- ❌ 長時間実行のステートフルサービス
- ❌ OSレベルの依存が多いアプリ
4. エッジでのAI推論
軽量MLモデルをWasmで動かすことが実用的になってきました:
// WasmでONNX推論を実行 use wasi_nn::{Graph, GraphEncoding, ExecutionTarget, Tensor}; fn classify_image(image_data: &[u8]) -> Vec<f32> { // ONNXモデルのロード let graph = Graph::load( &[model_bytes], GraphEncoding::Onnx, ExecutionTarget::Cpu, ).unwrap(); let context = graph.init_execution_context().unwrap(); // 入力テンソルの設定 context.set_input(0, Tensor::new( &[1, 3, 224, 224], // batch, channels, height, width TensorType::F32, image_data, )).unwrap(); // 推論実行 context.compute().unwrap(); // 出力取得 context.get_output(0).unwrap() }
特にエッジAIで魅力的です。同じWasmバイナリが、Cloudflare Workers上でも、ブラウザ上でも、IoTデバイス上でも動作します。
パフォーマンス:実際の数字を見てみる
スタートアップ時間
コールドスタート比較(シンプルなHTTPハンドラー):
┌──────────────────────────────────────────────┐
│ Docker (Node.js): ~500ms │
│ Docker (Go): ~100ms │
│ AWS Lambda (Node): ~200ms │
│ Wasm (Wasmtime): ~1ms │
│ Wasm (Spin): ~1ms │
└──────────────────────────────────────────────┘
サーバーレスプラットフォームがWasmを好む理由がこの100〜500倍の差です。Scale-to-zeroワークロードでは、スタートアップ時間がそのままレイテンシーになります。
スループット
CPU集約型の処理では、Wasmはネイティブの80〜95%の速度で動きます。差は大きく縮まっています:
JSONパースベンチマーク(1MBペイロード):
┌──────────────────────────────────────────────┐
│ ネイティブRust: 12.3ms │
│ Wasm (Wasmtime): 14.1ms (ネイティブの87%)│
│ Node.js: 28.7ms │
│ Python: 89.4ms │
└──────────────────────────────────────────────┘
メモリ
メモリフットプリント(アイドルHTTPサーバー):
┌──────────────────────────────────────────────┐
│ Node.js: ~50MB │
│ Go: ~15MB │
│ Wasm (Spin): ~2MB │
└──────────────────────────────────────────────┘
Wasmが速くない場面
正直に言うと、Wasmが速くないケースもあります:
- I/Oが多い処理: WASI抽象化レイヤーがファイル/ネットワーク操作にオーバーヘッドを追加
- 長時間の演算: SIMDとスレッドを活用するネイティブコードにはまだ及ばない
- GPU処理: WASIにGPUアクセスがないため、AIトレーニングやグラフィックスは対象外
手を動かす:最初のWASIアプリ
Spinで始める(Fermyon)
サーバーサイドWasmを一番手軽に始める方法:
# Spinをインストール curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash # プロジェクト作成 spin new -t http-rust my-api cd my-api
生成されるコード:
// src/lib.rs use spin_sdk::http::{IntoResponse, Request, Response}; use spin_sdk::http_component; #[http_component] fn handle_request(req: Request) -> anyhow::Result<impl IntoResponse> { let path = req.uri().path(); match path { "/" => Ok(Response::builder() .status(200) .header("content-type", "application/json") .body(r#"{"status": "ok", "runtime": "wasm"}"#)?), "/heavy-compute" => { // ネイティブに近い速度で実行 let result = fibonacci(40); Ok(Response::builder() .status(200) .body(format!(r#"{{"fib40": {}}}"#, result))?) } _ => Ok(Response::builder() .status(404) .body("Not found")?), } } fn fibonacci(n: u64) -> u64 { match n { 0 => 0, 1 => 1, _ => fibonacci(n - 1) + fibonacci(n - 2), } }
# spin.toml spin_manifest_version = 2 [application] name = "my-api" version = "0.1.0" [[trigger.http]] route = "/..." component = "my-api" [component.my-api] source = "target/wasm32-wasip2/release/my_api.wasm" [component.my-api.build] command = "cargo build --target wasm32-wasip2 --release"
# ビルドしてローカルで実行 spin build spin up # テスト curl http://localhost:3000/ # {"status": "ok", "runtime": "wasm"} # Fermyon Cloudにデプロイ spin deploy
wasmtimeを直接使う
より細かいコントロールが必要な場合:
# wasmtimeをインストール curl https://wasmtime.dev/install.sh -sSf | bash # WASIターゲットでRustアプリをコンパイル rustup target add wasm32-wasip2 cargo build --target wasm32-wasip2 --release # 実行 wasmtime run target/wasm32-wasip2/release/myapp.wasm
2026年のツール事情
ランタイム
| ランタイム | 特化分野 | 本番利用可 | 備考 |
|---|---|---|---|
| Wasmtime | 汎用 | ✅ | Bytecode Alliance、最も成熟 |
| Wasmer | ユニバーサル | ✅ | レジストリ、WAPM、パッケージマネージャー |
| WasmEdge | エッジ/AI | ✅ | CNCFプロジェクト、ONNXサポート |
| Spin | サーバーレス | ✅ | Fermyon、最も良いDX |
| Wazero | Go埋め込み | ✅ | Pure Go、CGo不要 |
言語ごとの準備状況
すべての言語がWASI開発に同じように準備できているわけではありません:
| 言語 | コンポーネントモデル | WASI 0.3 | 成熟度 |
|---|---|---|---|
| Rust | ✅ フルサポート | ✅ | プロダクション |
| Go(TinyGo) | ✅ | ✅ | プロダクション |
| Python(componentize-py) | ✅ | ⚠️ 部分対応 | ベータ |
| JavaScript(ComponentizeJS) | ✅ | ⚠️ 部分対応 | ベータ |
| C/C++ | ✅ | ✅ | プロダクション |
| C#/.NET | ✅(実験的) | ⚠️ | アルファ |
主要ツール
# wasm-tools: Wasmの万能ツール cargo install wasm-tools # コンポーネントの情報を確認 wasm-tools component wit myapp.wasm # コンポーネントを合成 wasm-tools compose main.wasm --adapt adapter.wasm -o composed.wasm # jco: JavaScript Component Tools npm install -g @bytecodealliance/jco # WasmコンポーネントをJSモジュールに変換 jco transpile component.wasm -o output/ # cargo-component: Rustコンポーネントのビルド cargo install cargo-component cargo component new my-component cargo component build
実際のアーキテクチャ例
コンポーネントモデルで構成したサーバーがどうなるか、見てみましょう:
┌─────────────────────────────────────────────────────────┐
│ APIゲートウェイ │
│ (Wasmコンポーネント) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 認証 │ │ Rate Limiter │ │ ロガー │ │
│ │ (Rust) │ │ (Go) │ │ (Python) │ │
│ │ コンポーネント│ │ コンポーネント │ │ コンポーネント │ │
│ └──────┬──────┘ └──────┬───────┘ └──────┬───────┘ │
│ └────────────────┼──────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ ビジネスロジック │ │
│ │ (任意の言語) │ │
│ │ ┌──────────────────┐ │ │
│ │ │ 画像プロセッサ │ │ │
│ │ │ (Rustコンポーネント) │ │ │
│ │ └──────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
各コンポーネントは:
- 最適な言語で作成
- 必要な権限のみ付与されたサンドボックス
- WITインターフェースを通じた他コンポーネントとの結合
- ホスト再起動なしでのホットスワップが可能
ハマりがちなポイント
1.「Node.jsアプリをそのままWasmにコンパイルすればいい」
そうはいきません。Node.jsのAPIはWASIには存在しません。SpinやWasmCloudなど、WASI向けに設計されたフレームワークを使ってください。
2. フルPOSIX互換を期待しない
WASIはPOSIXではありません。新しく、最小限のインターフェースです。fork()、共有メモリ、シグナルは存在しません。WASIのモデルに合わせたアプリケーション設計が必要です。
3. バイナリサイズの管理
最適化しないとWasmバイナリは大きくなります:
# Rust: サイズ最適化設定 [profile.release] opt-level = "s" # サイズ最適化 lto = true # リンクタイム最適化 strip = true # デバッグ情報除去 codegen-units = 1 # より良い最適化 # 結果: ~5MB → ~500KB
4. すべての言語が平等ではない
RustとC/C++が最も優れたWasmを生成します。Go(TinyGo経由)は良好ですが制約もあります。PythonとJavaScriptは、インタープリタをWasmにコンパイルする方式のため、オーバーヘッドがあります。
今後のロードマップ
現在(2026 Q1)
- ✅ WASI 0.3.0リリース(非同期I/O)
- ✅ コンポーネントモデル安定化が進行中
- ✅ Docker Wasmサポートがプロダクション利用可能
- ✅ Cloudflare、Fastly、Fermyonがプロダクション運用中
2026 Q2〜Q3
- WASIスレッド提案の進展
- コンポーネントレジストリ(コンポーネントのパッケージ管理)
- 言語サポートの拡大(Java、Swift、Kotlinコンポーネント)
wasi-nn(AI推論インターフェース)の安定化
2026後半〜2027年
- WASI 1.0正式リリース予定
- GPUアクセス提案
- エッジコンピューティングを超えた幅広い業界導入
- Kubernetesネイティブスケジューリングとの統合の可能性
最終的なゴール
- どの言語でもコードを書いて
- ひとつのユニバーサルバイナリ(Wasmコンポーネント)にコンパイルし
- どこでも実行:ブラウザ、サーバー、エッジ、IoT、組み込み
- 他言語のコンポーネントと組み合わせ
- セキュリティはデフォルト:サンドボックス化、ケーパビリティベース、暗黙の権限なし
Docker共同創設者のSolomon Hykesの言葉:「2008年にWASM+WASIが存在していたら、Dockerを作る必要はなかっただろう。それほど重要な技術だ。」
まとめ
ブラウザの外のWebAssembly、もう未来の話じゃないです。具体的で実用的なユースケースがあります:
- エッジコンピューティング: コンテナ比100〜500倍速いコールドスタート
- プラグインシステム: 安全なサンドボックス、多言語プラグイン
- サーバーレス: マイクロ秒で起動、メガバイトのメモリ
- コンポーネント合成: 言語に関係なくひとつのアプリに統合
今日から試せること:
- 今日:
wasmtimeをインストールしてWASI hello worldを実行(5分です) - 今週: SpinでシンプルなHTTPエンドポイントを作ってFermyon Cloudに無料デプロイ
- 今月: マイクロサービスの中でWasmに置き換えられるものがないか検討
- 今四半期: コンポーネントモデルを実践。異なる言語のコンポーネント同士を組み合わせてみる
ブラウザはただの始まりでした。Wasmはエッジから内側へ、インフラ全体に広がっています。コンポーネントモデルが、「面白い技術」から「プラットフォームシフト」に変える最後のピースです。
関連ツールを見る
Pockitの無料開発者ツールを試してみましょう