WebAssembly
WebAssembly ・ うぇぶあせんぶり
ブラウザでネイティブ並みの速度で動くバイナリ形式。JSの補完としてWebの限界を広げる。
✦ 30秒で試す — 航海ブラウザの中で、C や Rust のプログラムがほぼ素の速さで走るwasm が画面の文字を書き換えるには、どうする?予想してみる概要
WebAssembly(wasm)は、ブラウザの中でネイティブに近い速度で実行できる、コンパクトなバイナリ形式の命令セットです。C・C++・Rust などで書いたプログラムをコンパイラで wasm のバイナリに変換し、それをブラウザの JavaScript エンジンが検証して機械語に直して実行します。人が直接書く言語ではなく、さまざまな言語の「コンパイル先」として設計されている点が特徴です。
主要なブラウザすべてが対応する Web 標準で、Figma(C++ 製の描画エンジン)、Google Meet の背景ぼかし、ブラウザ版 Photoshop、ゲームエンジンの Web 版など、「JavaScript では速度が足りない」領域で使われています。JavaScript を置き換えるものではなく、計算の重い部分を引き受ける補完役 — これが WebAssembly の立ち位置です。画面(DOM)の操作は相変わらず JavaScript の仕事で、wasm は JavaScript から呼ばれ、JavaScript の関数を呼び返して外の世界とつながります。
なぜ生まれたか
長いあいだ、ブラウザで動く言語は JavaScript だけでした。JavaScript はエンジンの JIT コンパイラ(実行しながら、よく通る部分を機械語に変換する仕組み)の進化で大きく速くなったものの、動的型付け(型システム)の言語なので、「この変数はずっと整数だろう」という推測が外れるたびに最適化をやり直す必要があり、速度が安定しません。画像処理・物理演算・ゲームのような計算の重い処理では、ネイティブアプリとの差が埋まりませんでした。Flash や Java アプレット、Google の NaCl のようなプラグイン・独自技術で補う試みもありましたが、セキュリティとブラウザ間の互換性の問題から廃れていきます。
転機は 2013 年の asm.js(Mozilla)でした。JavaScript のうち、型が推測しやすく JIT が最適化しやすい書き方だけに絞った部分集合を決め、C/C++ を Emscripten というコンパイラでその形に変換すると、ブラウザを変えずにネイティブの半分程度の速度が出る — この実証が「JavaScript の文字列を経由するのではなく、最初から実行のために設計したバイナリ形式を標準にすればよい」という合意につながりました。主要ブラウザベンダーが共同で設計し、2017 年に WebAssembly が全主要ブラウザに載り、2019 年に W3C の勧告になります。テキストの JavaScript を構文解析するより、型の決まったバイナリを検証するほうが、ダウンロードも起動も実行も速くできるのです。
詳細
実行モデル — スタックマシンと線形メモリ
wasm の実行モデルは意図的に単純です。命令は「値をスタックに積み、演算で取り出して結果を積み直す」スタックマシン方式で、関数の引数と戻り値、ローカル変数にはすべて型(32/64 ビットの整数と浮動小数点数が基本)が付いています。型が静的に決まっているので、エンジンはバイナリを読み込みながら一度で検証し、推測に頼らずに効率のよい機械語へ変換できます。ブラウザは wasm をダウンロードしながらコンパイルを進められる(WebAssembly.instantiateStreaming)ので、大きなモジュールでも起動が速くなります。
メモリは「線形メモリ」と呼ばれる、連続したバイトの列です。大きさは 64 KiB の「ページ」単位で、必要に応じて伸ばせます(memory.grow)。プログラムは番地(先頭からのオフセット)を指定して読み書きするので、C のポインタ演算がそのまま移植できます。一方で、すべての読み書きは線形メモリの範囲内かどうか確かめられ、外を指せばその場で「トラップ」して止まります。wasm のコードからは、線形メモリの外 — JavaScript のオブジェクトや、ブラウザそのもののメモリ — には、どんな番地を計算しても届きません。ネイティブ並みの速さと、ブラウザに求められる安全性を、この設計が両立させています。
JavaScript との分業 — wasm は DOM に触れない
重要な制約として、wasm のモジュールは DOM やネットワークなどのブラウザの機能を直接呼べません。できるのは数値の計算と線形メモリの読み書き、そして「インポートされた関数」の呼び出しだけです。モジュールはバイナリの中に「外から何を借りたいか」(インポート)を宣言していて、JavaScript はインスタンスを作るときにその関数を渡します。画面を更新したければ、wasm は計算結果を線形メモリに書いてから、インポートした JavaScript の関数を「この番地から何バイト」と呼び、JavaScript がそのバイト列を読んで DOM に反映します。JavaScript 側は new Uint8Array(memory.buffer) のように、線形メモリを型付き配列(TypedArray)として直接覗けるので、コピーなしで同じバイトを共有できます。
次の部品は、ブラウザ標準の WebAssembly API で、手で組み立てた小さなモジュールを本当に動かしています。金のつまみで番地を指して読み書きし、範囲の外へ動かしたり、インポートの線を切ったりしてみてください。
JS の帯のマスを押すと、JS が Uint8Array で大文字・小文字を入れ替えます。インポートの線を押すと、show を渡さずにインスタンス化し直します。
金のつまみ(ptr)をドラッグして、線形メモリの好きな番地を指してください。つまみを押して文字キーを打つと、wasm がその番地に書き込みます。
実際のアプリは「UI とグルー(接着)コードは JavaScript、重い計算は wasm」という分業になります。JavaScript と wasm の呼び出しの境界にはわずかなコストがあり、文字列やオブジェクトはそのままでは渡せない(バイト列に直して線形メモリ経由で渡す)ので、細かく何度も行き来するのではなく「大きな仕事をまとめて渡す」のが速さを出すコツです。wasm-bindgen(Rust)や Emscripten(C/C++)は、この変換のグルーコードを自動生成してくれる道具です。落とし穴として、memory.grow でメモリが伸びると古い ArrayBuffer は切り離され、作ってあった TypedArray は長さ 0 になります。伸びたあとは作り直す必要があります。
MVP から先へ — SIMD・参照型・GC
2017 年に出た最初の版(MVP = 最小限の実用版)の wasm は、「整数と浮動小数点数だけを扱い、メモリは線形メモリ1本」という、C や C++、Rust のように自分でメモリを管理する言語のための設計でした。その後、命令と型が少しずつ足されています。128 ビットの SIMD(1命令で複数の値をまとめて計算する。画像や音声の処理が大きく速くなる)、JavaScript のオブジェクトなどを不透明な参照として持てる参照型、例外処理、末尾呼び出しなどです。
中でも大きいのが WasmGC(ガベージコレクションの拡張)です。MVP の wasm で Java や Kotlin のようなガベージコレクションを前提とする言語を動かすには、GC そのものを wasm にコンパイルして線形メモリの中で動かすしかなく、バイナリが大きく、ブラウザの GC とも協調できませんでした。WasmGC は、構造体や配列を wasm の型として宣言し、その回収をブラウザの GC に任せられるようにしたもので、Chrome 119・Firefox 120(2023 年)、Safari 18.2(2024 年)で使えるようになりました。Kotlin/Wasm や Dart(Flutter の Web 版)、Java を変換する J2Wasm などがこれを使っています。2025 年 9 月には GC・64 ビットの線形メモリ(memory64)・例外処理などをまとめた Wasm 3.0 の仕様が完成しました。「メモリは線形メモリ1本、型は数値だけ」は、いまでは wasm の基本形であって全部ではありません。
どうやって wasm を作るか
wasm は原則として、既存の言語からコンパイルして作ります。C/C++ からは Emscripten、Rust からは標準のツールチェーンと wasm-bindgen、Go や C#(Blazor)、Kotlin なども対応しています。既存の C/C++ の資産 — 画像コーデック、暗号ライブラリ、SQLite のようなデータベース、ゲームエンジン — を書き直さずに Web へ持ち込めることが、実務で wasm が選ばれる一番の理由です。TypeScript を JavaScript にするトランスパイルが「ソースからソースへ」の変換であるのに対し、wasm へのコンパイルは低い水準のバイナリへ落とす通常の意味でのコンパイルです。
一方で、何でも wasm にすれば速くなるわけではありません。DOM を頻繁に触る UI のコードは JavaScript のほうが速く書けて速く動きますし、JavaScript の JIT も十分に速いので、差が出るのは計算が大半を占める処理です。バイナリの大きさ(GC やランタイムを抱えると数 MB になる)と、デバッグのしにくさも考えに入れます。
ブラウザの外へ — WASI と「軽量な実行単位」
wasm の「速く・安全で・どこでも動くサンドボックス」という性質は、ブラウザの外でも価値があります。ブラウザの外で動かすための標準の窓口が WASI(WebAssembly System Interface)で、ファイルや時刻、ネットワークといった OS の機能への、権限を明示的に絞ったアクセス方法を定めています。ブラウザで JavaScript がインポートを渡していた役を、WASI のランタイム(Wasmtime など)が担うわけです。ミリ秒未満で起動できる軽さから、CDN のエッジで動くサーバレスの実行環境や、アプリのプラグイン機構(利用者が書いたコードを安全に動かす仕組み)として採用が進んでいます。
コンテナが「OS の上でプロセスを隔離する」のに対し、wasm は「1つのモジュールを命令のレベルで隔離する」より細かい単位です。起動の速さと隔離の強さで勝る一方、既存のアプリをそのまま動かす互換性ではコンテナに及びません。両者は置き換えではなく、隔離の粒度が違う道具として使い分けられています。
試問
画面の上段には、ページの画面(DOM)・JavaScript・wasm モジュールが並び、下段には線形メモリ(wasm と JS が共有するバイト列)が、wasm から見た帯と JS の Uint8Array から見た帯の2本で表示されています。JS の帯のマスを押すと JS がその英字の大文字・小文字を入れ替え、「wasm: render → 画面に出す」を押すと wasm がメモリの先頭の文字列を画面に出そうとします。いまメモリには Hi, wasm! が入っています。
JS の帯のマスを押すと、JS が Uint8Array で大文字・小文字を入れ替えます。インポートの線を押すと、show を渡さずにインスタンス化し直します。
金のつまみ(ptr)をドラッグして、線形メモリの好きな番地を指してください。つまみを押して文字キーを打つと、wasm がその番地に書き込みます。
