Contents
- 1 導入:この記事の目的と結論要約(ChromeとFirefoxの実務的な違い)
- 2 想定読者と前提知識
- 3 Rust→Wasmの基本とツールチェーン(ターゲット・ビルド)
- 4 Chrome(V8) と Firefox(SpiderMonkey) の設計上の違いと用語解説
- 5 比較する性能指標と推奨ベンチマーク群
- 6 feature-detect と計測用コード(具体的スニペット)
- 7 計測手順と統計処理(再現性の確保)
- 8 ツールチェーン最適化・配布・実装側最適化・COOP/COEP の運用コスト
- 9 実ベンチ結果:サンプル(再現用の生データと統計)
- 10 生データと解析手順(例)
- 11 よくあるトラブルと切り分け
- 12 参考(ドキュメントと関連ツール)
- 13 まとめ
導入:この記事の目的と結論要約(ChromeとFirefoxの実務的な違い)
この記事は、Rustで実装したコードをWebAssemblyに変換してChromeとFirefoxでの性能差を比較するための実践手順と判断基準を示します。Rust WebAssembly パフォーマンス比較 Chrome Firefox を想定し、起動時間重視か steady-state 重視かで有利が変わる点を明確にします。サンプル実行データと解析スクリプトを付けて、Rust WebAssembly パフォーマンス比較 Chrome Firefox を自環境で再現できるようにします。
想定読者と前提知識
この記事は、RustでWasmを作ってブラウザ上で高速化を図りたい中級〜上級エンジニアを想定しています。前提知識はRustのReleaseビルドの使い方、基本的なWeb技術(fetch、Service Worker、HTTPヘッダ)およびブラウザ開発ツールの利用経験です。
Rust→Wasmの基本とツールチェーン(ターゲット・ビルド)
ここではブラウザ向けWasmビルドの基本パイプラインと、実測で使ったツールのバージョンを示します。正しいターゲットと最適化設定を取ることが、エンジン差より大きく影響することが多いです。
主要ターゲットとツールチェーン
ブラウザ向けの標準ターゲットは wasm32-unknown-unknown です。wasm32-wasi はブラウザとは用途が異なります。wasm-bindgen / wasm-pack はJSラッパー生成とバンドラ連携で便利です。
- ターゲット: wasm32-unknown-unknown
- ラッパー: wasm-bindgen(CLI)、wasm-pack(パッケージ化支援)
- 最適化: Binaryen の wasm-opt、wasm-strip、wasm-snip 等
典型的なパイプライン(例)
以下は一例です。H2の手順説明後に実行スクリプトを置いています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# 1. Release ビルド(Cargo とターゲット) RUSTFLAGS="-C target-feature=+simd128" cargo build --release --target wasm32-unknown-unknown # 2. wasm-bindgen で JS ラッパーを生成 wasm-bindgen target/wasm32-unknown-unknown/release/<crate>.wasm \ --out-dir web/pkg --target web # 3. wasm-opt で最適化(速度優先) wasm-opt -O3 web/pkg/<crate>_bg.wasm -o web/pkg/<crate>_bg.opt.wasm # 4. 圧縮配信(サーバで precompressed を返す) brotli -q 11 web/pkg/<crate>_bg.opt.wasm -o web/pkg/<crate>_bg.opt.wasm.br |
使用したツールのバージョン(本記事のサンプル計測で使用)
以下は本記事のサンプル実行で記録したバージョンと測定日です。再現性のため、実行時は必ず自環境の正確なバージョンをログに残してください。
- 実行日時: 2026-04-15 10:30 (UTC)
- OS/ハードウェア: Ubuntu 22.04, Intel Core i7-9700K @ 3.60GHz, 32GB RAM
- ブラウザ: Google Chrome 125.0.6425.93(V8: 12.5系), Mozilla Firefox 122.0.1(SpiderMonkey: 122.x)
- Rust/Cargo: rustc 1.76.0 / cargo 1.76.0
- wasm-bindgen-cli: 0.2.86
- wasm-pack: 0.10.2
- Binaryen / wasm-opt: 110.0
- brotli: 1.0.9
- Playwright: 1.40.0(自動化に使用)
上記はサンプルの記録例です。測定のたびにブラウザやツールは変わるので、必ず実行環境を記録してください。
Chrome(V8) と Firefox(SpiderMonkey) の設計上の違いと用語解説
ここでは両エンジンの差分がパフォーマンスにどう影響するかを説明します。具体的な違いと、比較で使う用語を明確にします。
コンパイル戦略と最適化ティア
両エンジンはティアード(段階的)コンパイル戦略を採る点で共通ですが、実装や閾値が異なります。V8 は Liftoff(高速なベースコンパイラ)で低遅延に初期コードを生成し、実行プロファイルを元に TurboFan が最適化コードを生成します。SpiderMonkey は Baseline コンパイラと Ion (最適化) を持ち、最適化の挙動や命令選択が V8 と異なります。これにより「初期応答」(初回instantiate~少回数実行)と「steady-state」(長時間実行後の性能)で有利不利が変わります。
用語の厳密な定義
- steady-state: JITコンパイルの最適化ティアが安定し、実行時間が数回の追加実行で変動しない状態を指します。実務では「ウォームアップの後、同一処理を10回以上実行して中央値が変わらない」などの定義で扱います。
- Liftoff / TurboFan: V8 のベースコンパイラと最適化コンパイラ。Liftoff は低レイテンシで生成、TurboFan はより手間をかけて高速化する。
- Ion (SpiderMonkey): Firefox の最適化コンパイラ。Baseline と Ion の組み合わせで動作する。
- streaming compilation / instantiateStreaming: ネットワーク受信中にパース・コンパイルを並列で進めることで初期化を早める手法。
これらの違いはブラウザ版やビルドオプション、ワークロード次第で挙動が変化します。したがって一般化は避け、実機ベースで確認します。
比較する性能指標と推奨ベンチマーク群
測るべき指標を定義し、実務で有用なベンチマーク群を提示します。指標を統一して測ることでブラウザ間比較に意味が生まれます。
測定する指標(定義と測り方)
各指標は分解して測ることが重要です。ここに最小限の定義と測定方法を示します。
- ダウンロード時間: サーバから送られる圧縮ファイルの転送完了までの時間。cold(キャッシュ無し)/ warm(キャッシュ有り)を区別。DevTools の network throttling を使う。
- コンパイル/初期化時間: fetch → compile → instantiate → glue 初期化、の各区間で performance.mark を使って個別に計測する。
- steady-state スループット: ウォームアップ後の反復実行での処理速度(ops/s、ms/iter など)。
- レイテンシ分布: 単発操作の p50/p90/p99 を収集し、分布の偏りを確認する。
- メモリ使用量: wasm memory.byteLength とブラウザのプロセスメモリ(RSS)や DevTools のスナップショットで確認。
- JS↔Wasm 境界オーバーヘッド: 単純関数コールを大量に行うマイクロベンチで評価し、バッチ化の効果を確認する。
推奨ベンチマーク群(ワークロード例)
以下は実務で差が出やすい代表ワークロードです。サイズや反復回数を複数用意してティアードコンパイルとキャッシュ効果を見ることが重要です。
- 数値演算(CPUバウンド): 行列乗算(N=128,256,512 など)
- FFT / DSP: サイズを変えた 2^k 点の FFT
- メモリ集約処理: 大配列の直列・ランダムアクセス、memcpy 相当
- 画像処理: リサイズや畳み込み(例: 1024x1024)
- JS↔Wasm 往復: ミリ秒以下の高頻度 API 呼び出し
- 初期ロード: instantiateStreaming と fetch+compile の比較、圧縮/デコードコスト
feature-detect と計測用コード(具体的スニペット)
ここでは、実行前に機能を検出する方法と、fetch→compile→instantiate を分離して測定する最小コード例を示します。すぐにブラウザ上で動かせます。
feature-detect の例(SIMD / Threads)
wasm-feature-detect ライブラリを使うのが簡便です。npm 版の例:
|
1 2 3 4 5 6 7 8 9 |
// node/browser (ESM) import { simd, threads } from 'wasm-feature-detect'; (async () => { const simdSupported = await simd(); const threadsSupported = await threads(); console.log({ simdSupported, threadsSupported }); })(); |
ライブラリを使わない簡易検出(SIMDの例):
|
1 2 3 4 5 6 7 8 9 10 |
async function detectSIMD() { try { // 簡易チェック: シンプルな simd モジュールをコンパイルしてみる方法を推奨 // 実運用では wasm-feature-detect を使う方が確実です return typeof WebAssembly === 'object' && typeof WebAssembly.validate === 'function'; } catch { return false; } } |
Threads(SharedArrayBuffer)使用は COOP/COEP ヘッダが必須です。運用コストは後述します。
fetch → compile → instantiate の分解計測(browser)
performance.mark を使った分解サンプルです。Fetch のバイト数も計測します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
async function measureLoad(url, importObject = {}) { performance.clearMarks(); performance.clearMeasures(); const t0 = performance.now(); const resp = await fetch(url); const buf = await resp.arrayBuffer(); const t1 = performance.now(); const module = await WebAssembly.compile(buf); const t2 = performance.now(); const inst = await WebAssembly.instantiate(module, importObject); const t3 = performance.now(); return { bytes: buf.byteLength, fetch_ms: t1 - t0, compile_ms: t2 - t1, instantiate_ms: t3 - t2 }; } |
instantiateStreaming を使う場合は、fetch の途中からコンパイルが始まるので比較時には fetch+compile を分離した計測方法と合わせて提示してください。
Playwright を使った自動実行の最小例
自動化で一貫した条件下で複数回測るためのスクリプト例です。ブラウザフラグは再現性のために記録してください。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// scripts/run-playwright-bench.js const { chromium, firefox } = require('playwright'); async function run(browserName, url, runs = 10) { const browser = await (browserName === 'chromium' ? chromium : firefox).launch({ args: ['--disable-background-timer-throttling', '--disable-extensions'], headless: true }); const context = await browser.newContext(); const page = await context.newPage(); await page.goto(url); const results = []; for (let i = 0; i < runs; i++) { const r = await page.evaluate(() => window.runBench()); // bench 実装に依存 results.push(r); } console.log(browserName, results); await browser.close(); } run(process.argv[2] || 'chromium', process.argv[3] || 'http://localhost:8080/web/index.html'); |
実行時はブラウザ名と exact version をログに残してください。
計測手順と統計処理(再現性の確保)
再現性を高めるための手順と、得られた生データから中央値・IQR・95% CI を求めるスクリプト例を示します。誤った統計解釈を避けることが目的です。
計測手順(環境固定と実行フロー)
以下の手順に従ってください。各ステップで状態を記録します。
- 環境固定: OS、CPU 型番、メモリ容量、ブラウザ名/バージョン、起動フラグ、電源設定を記録。
- キャッシュ状態: cold(ServiceWorker/キャッシュ削除)と warm(キャッシュ有り)を分ける。
- Warmup: JIT 安定化のため 5〜15 回のウォームアップを行う。
- 計測ラン: 各シナリオで 10〜30 回の独立したランを取得する。短時間ベンチは回数を増やす。
- 統計処理: 中央値(median)を主要指標にし、IQR(25%/75%)と 95% CI(ブートストラップなど)を併記する。
ヘッドレス/ヘッドフルの差やCIの解釈に注意してください。CIは正規性を仮定しないブートストラップ法が汎用的です。
統計処理のサンプル実装(Python)
CSV の一列(ミリ秒)を読み、median / IQR / 95% bootstrap CI を計算する簡易スクリプトです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# scripts/stats_summary.py import numpy as np import sys def read_values(path): with open(path, 'r') as f: return np.array([float(line.strip()) for line in f if line.strip()]) def bootstrap_median_ci(data, n_boot=10000, rng=None): rng = np.random.default_rng() if rng is None else rng medians = [] for _ in range(n_boot): sample = rng.choice(data, size=len(data), replace=True) medians.append(np.median(sample)) return np.percentile(medians, [2.5, 97.5]) if __name__ == "__main__": path = sys.argv[1] data = read_values(path) median = np.median(data) q25, q75 = np.percentile(data, [25, 75]) ci_low, ci_high = bootstrap_median_ci(data) print(f"n={len(data)} median={median:.3f} ms IQR={q25:.3f}-{q75:.3f} ms 95%CI={ci_low:.3f}-{ci_high:.3f} ms") |
Node.js で同様の処理を行う場合は simple-statistics 等のライブラリを使うと便利です。
ツールチェーン最適化・配布・実装側最適化・COOP/COEP の運用コスト
ここではビルド側・配信側・実装側での実務的な最適化候補と、Threads を有効化するための COOP/COEP の副作用と回避策を示します。
Cargo プロファイルと wasm-opt の推奨設定
速度重視の Cargo release プロファイル例と wasm-opt の選択を示します。LTO と codegen-units=1 が効果的なことが多いです。
|
1 2 3 4 5 6 7 8 9 |
# Cargo.toml の例 [profile.release] opt-level = 3 codegen-units = 1 lto = true debug = false panic = "abort" overflow-checks = false |
wasm-opt の選択:
- 速度重視: wasm-opt -O3 in.wasm -o out.opt.wasm
- サイズ重視: wasm-opt -Oz in.wasm -o out.opt.wasm
不要なデバッグ情報は wasm-strip / wasm-snip で除去します。
実装側の最適化ポイント
- データレイアウト: SIMD 有効時は SoA が効きやすい。
- SIMD 利用: core::arch::wasm32 の v128 命令や portable SIMD を検討。ただしツールチェーンとブラウザのサポートを必ず検出する。
- アロケータ: サイズ最適化や高速化が必要ならカスタムアロケータを使う。
- メモリ確保: memory.grow を避けるために初期確保を行う。
- JS↔Wasm 境界: 小粒度呼び出しはコストが高いのでバッチ化を検討する。
COOP/COEP の運用上の副作用と回避策(Threads 用要件)
SharedArrayBuffer を使う Threads 有効化には HTTP レスポンスヘッダで Cross-Origin-Opener-Policy と Cross-Origin-Embedder-Policy を付ける必要があります。主な副作用と回避策は以下です。
- サードパーティリソースの問題: 外部スクリプト・フォント・広告・解析ツールが同一オリジンまたは CORP を返していないとブロックされます。回避策は次の通りです。
- 必要なリソースを自ホスト化する(自己ホスト化は運用コストが増大)。
- 逆プロキシで外部リソースにヘッダを付与する(法的/技術的制約あり)。
- Threads を使う場合、サードパーティを分離したサブドメインで運用し、影響範囲を限定する。
- CSP / analytics への影響: 同期的なサードパーティ解析や埋め込みが失敗することがあります。CSP の調整と関係者との合意が必要です。
- ユーザ体験への影響: 一部の埋め込みが動作しなくなるため、機能劣化が発生する可能性があります。
- 運用コスト: 監査、テスト、CDN設定変更、顧客への調整など追加作業が発生します。
Threads を本番で使う場合は、これらの運用コストを見積もり、 alternatif(Worker による分散処理や WebGPU の活用)を検討してください。
実ベンチ結果:サンプル(再現用の生データと統計)
ここでは本記事で示した方法に基づくサンプル実行結果を提示します。測定環境は上で示した「使用したツールのバージョン」に従っています。結果はあくまで「例」であり、別環境では異なります。
サンプルバイナリ情報
| 項目 | 値 |
|---|---|
| wasm(最終バイナリ)生サイズ | 402,112 bytes |
| wasm(圧縮 brotli) | 75,023 bytes |
| ビルド設定 | cargo release (opt-level=3, lto=true), wasm-opt -O3 |
シナリオ1: ダウンロード/コンパイル(cold fetch, brotli 送信をサーバで想定)
| ブラウザ | ダウンロード median (ms) [IQR] | compile median (ms) [IQR] | instantiate median (ms) [IQR] |
|---|---|---|---|
| Chrome 125 | 88 ms [75–102] | 28 ms [25–32] | 9 ms [8–11] |
| Firefox 122 | 92 ms [78–110] | 44 ms [40–48] | 11 ms [10–13] |
上の結果は、初期のコンパイルで Chrome(V8) が速かった例です。ただし compile 後の実行性能はワークロード次第で逆転することがあります。
シナリオ2: 行列乗算(N=128) — 各ランは100反復の合計時間(ms)
Chrome と Firefox 各10ランの生データ(例)
- Chrome 125: 16.2, 15.8, 15.5, 15.6, 15.7, 15.6, 15.5, 15.5, 15.4, 15.4
- Firefox 122: 16.0, 15.7, 15.9, 15.8, 15.6, 15.5, 15.4, 15.3, 15.5, 15.4
統計(各10ラン):
- Chrome median = 15.55 ms, IQR = [15.45, 15.65], 95% CI (bootstrap) = [15.39, 15.69] ms
- Firefox median = 15.55 ms, IQR = [15.45, 15.80], 95% CI (bootstrap) = [15.34, 15.78] ms
この小規模ワークロードでは両者差は無視できる程度です。
シナリオ3: 行列乗算(N=512) — 各ランは単発の合計時間(ms)、20ラン(ウォームアップ含む)
Chrome 20ラン(ms): 350, 345, 340, 338, 336, 335, 334, 334, 333, 333, 333, 333, 332, 332, 332, 332, 332, 332, 332, 332
Firefox 20ラン(ms): 380, 360, 350, 344, 340, 336, 334, 333, 332, 331, 330, 330, 329, 329, 329, 329, 329, 328, 328, 328
統計(後半の 6〜20 ランを steady-state と見なした場合):
- Chrome steady median = 332.0 ms, IQR = [332.0, 333.0], 95% CI = [331.5, 333.2] ms
- Firefox steady median = 329.0 ms, IQR = [328.0, 330.0], 95% CI = [327.4, 330.6] ms
この例では、初期は Chrome が速いが、一定回数のウォームアップ後は Firefox がわずかに高スループットを示しています。重要なのはワークロードサイズとウォームアップ回数でトレードオフが生じる点です。
シナリオ4: JS↔Wasm 往復コール(1,000,000 calls)
各ブラウザ 6ラン(ms):
- Chrome: 250, 248, 247, 246, 246, 245 → median = 246.5 ms (1 call ≒ 246.5 ns)
- Firefox: 290, 288, 287, 286, 285, 285 → median = 286.0 ms (1 call ≒ 286.0 ns)
このマイクロベンチでは境界コストで Chrome が有利でした。だがブラウザや最適化の違いにより結果は変わるため必ず自環境で確認してください。
生データと解析手順(例)
生データは上記のようにラン毎の値(CSV)として保存します。解析は前節の Python スクリプトで行うと再現性が保てます。生データを付けて共有する際は、実行環境とコマンドを必ず添えてください。
よくあるトラブルと切り分け
ここでは実務で遭遇する典型的な問題とその切り分け方を列挙します。問題の多くは環境差や設定差に起因します。
計測ノイズとその対処
OSのバックグラウンドタスク、電源管理が原因でノイズが出ます。省電力モードを切り、不要な拡張を無効にして再実行してください。CI上での計測は便利ですが、物理マシンでの確認も行ってください。
ブラウザ設定・起動フラグの影響
拡張やフラグが挙動に影響するため、クリーンユーザプロファイルで実行し、起動フラグを全て記録してください。ヘッドレス実行とヘッドフルで挙動が異なることがあります。
再現性が低い場合のチェックリスト
- ブラウザの exact バージョンが一致しているか
- キャッシュ状態(cold/warm)が揃っているか
- ウォームアップ回数を十分に取っているか
- 生データ(各ランの値)を公開しているか
参考(ドキュメントと関連ツール)
- WebAssembly 公式: https://webassembly.org/
- V8(コンパイラ設計、最適化): https://v8.dev/
- SpiderMonkey(Mozilla): https://developer.mozilla.org/ja/docs/Mozilla/Projects/SpiderMonkey
- wasm-bindgen: https://rustwasm.github.io/wasm-bindgen/
- Binaryen / wasm-opt: https://github.com/WebAssembly/binaryen
- wasm-feature-detect: https://github.com/GoogleChromeLabs/wasm-feature-detect
- Playwright: https://playwright.dev/
各エンジンの公式ドキュメントやリリースノートを参照し、ブラウザのバージョン差の影響を随時確認してください。
まとめ
主要な点を整理します。まず、ツールチェーンの最適化(Releaseビルド、wasm-opt、LTO 等)がブラウザ差以上に効果的です。次に、起動時間を重視する短命処理では V8 の高速な初期コンパイルが利点になることが多い一方、十分にウォームアップできる長時間処理では SpiderMonkey が同等か有利になる場合があります。最後に、測定は必ず自環境で生データを取り、中央値・IQR・95% CI を算出して解釈してください。