
2026/03/11 13:44
ウェブ上でWebAssemblyを第一級言語にする
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
(欠落しているポイントを組み込む):**
要約
WebAssembly(Wasm)は2017年のデビュー以来、共有メモリ、SIMD、例外処理、テールコール、64ビットメモリ、ガベージコレクション、大量メモリ命令、複数戻り値、および参照値などの機能を追加しながら進化してきました。これらの技術的進歩にもかかわらず、Wasmは「二次的」なウェブ言語として位置づけられ続けています。というのも、すべてのモジュールが依然としてJavaScriptを介してロードされ、ブラウザAPIにバインドされる必要があるからです。
主な障壁は2つあります:
- 面倒なロード – JavaScript が
を使って手動でモジュールをフェッチし、インスタンス化する必要があります。WebAssembly.instantiateStreaming - APIアクセスにはグルーコードが必要 – WasmメモリとJSオブジェクト間の変換は言語固有であり、ビルドの複雑さを増加させ、実行時にオーバーヘッド(例:TodoMVCベンチマークで45%の遅延)が発生します。
esm-integration の提案では、.wasm モジュールを <script type="module" src="/module.wasm"> で直接インポートできるようにし、ロードを簡素化しますが、グルーコード問題は解決されません。Clang/LLVMなどのコンパイラは依然としてベアWasmを出力し、開発者は JavaScript を埋め込む非公式ツールチェーンに頼ることが多いです。
WebAssembly コンポーネントモデル は解決策を提示します:コンポーネントは高レベルのIDL(WIT)と低レベルのWasmコードをバンドルし、ブラウザや他言語がインターフェースを直接インポートできるようにします。例えば、Rust コンポーネントは WIT を介して
import std:web/console し、console::log を呼び出すことができます。そのコンパイル済みコンポーネントは <script type="module" src="component.wasm"> でブラウザにロードされます。JavaScript はエクスポートされたインターフェース(例:画像デコーダ)をネイティブモジュールとして消費できます(import { Image } from "image-lib.wasm")。
Mozilla と Google がこのモデルの構築に協力しており、Jco や Wasmtime などのツールはすでに開発者が実験できるようになっています。プラットフォーム統合のための JavaScript 依存を排除することで、コンポーネントモデルはビルドの複雑さ低減、パフォーマンス向上、および言語間相互運用性の拡大を約束し、WebAssembly のウェブエコシステム全体での採用を加速させる可能性があります。
本文
この投稿は、2025年ミュンヘンで開催された WebAssembly CG(コア開発者グループ)の会議で行ったプレゼンテーションを拡張したものです。
WebAssembly は 2017 年の初リリース以来、大きく進化しました。
最初のバージョンは C や C++ のような低レベル言語にすぐに適合し、ウェブ上で効率的に動作する新しい種類のアプリケーションを実現できました。
それ以降、WebAssembly CG は言語のコア機能を大幅に拡張し、共有メモリ、SIMD、例外処理、テイル・コール、64 ビットメモリ、GC サポートなどを追加しました。さらに、バルクメモリ命令や複数戻り値、参照値といった小さな改善も加えられました。
これらの拡張により、より多くの言語が WebAssembly を効率的にターゲットできるようになりました。スタックスイッチングやスレッド化などまだ重要な作業は残っていますが、WebAssembly はネイティブと比較して多くの面でギャップを縮めています。
それでも、何かが足りず WebAssembly がウェブ上で広く採用されることを妨げているように感じます。
その原因は複数ありますが、核心は「WebAssembly はウェブ上で二級言語になっている」点です。新しい言語機能が追加されたにもかかわらず、WebAssembly はウェブプラットフォームと十分に統合されていません。
結果として開発者体験が悪く、必要な場合以外は WebAssembly を使わないという選択肢が多く、JavaScript の方が「簡単で十分」というケースがほとんどです。こうしたユーザーは大規模企業に限られやすく、WebAssembly の恩恵はウェブコミュニティ全体ではなく一部の大手企業へと限定されてしまいます。
この問題を解決することは難しく、CG は WebAssembly 言語そのものの拡張に注力してきました。言語が十分に成熟した今、この点をより深く掘り下げる時です。本稿ではまず問題点を詳細に検討し、その後 WebAssembly コンポーネントがどのように改善策になるかを説明します。
なぜ WebAssembly は二級言語なのか?
ウェブプラットフォームのスクリプト層は次のように構造化されています:
- JavaScript は直接ウェブプラットフォームと対話できます。
- WebAssembly は JavaScript と直接対話し、さらに JavaScript を介してウェブプラットフォームと対話します。
WebAssembly はウェブプラットフォームにアクセスできますが、JavaScript の特殊な機能を使わない限りは不可能です。
JavaScript はウェブ上で一次言語であり、WebAssembly はそうではありません。
これは意図的・悪意ある設計決定ではなく、JavaScript がウェブの最初のスクリプト言語として存在し、プラットフォームと共進化した結果です。しかし、この設計は WebAssembly のユーザーに大きな影響を与えます。
JavaScript の特殊機能とは?
本稿で取り上げる主なものは 2 つです:
- コードのロード
- Web API の使用
コードのロード
WebAssembly コードは必要以上に面倒にロードされます。JavaScript コードを読み込むには単純に
<script> タグを書けば済みます。
<script src="script.js"></script>
一方、現在 WebAssembly は
<script> タグで直接サポートされていないため、開発者は WebAssembly JS API を使って手動でロード・インスタンス化する必要があります。
let bytecode = fetch(import.meta.resolve('./module.wasm')); let imports = { /* ... */ }; let { exports } = await WebAssembly.instantiateStreaming(bytecode, imports);
この一連の API 呼び出しは煩雑で、同じ処理を行う方法はいくつか存在し、それぞれにトレードオフがあります。ほとんどの開発者は覚えることができず、ツールによって自動生成されるケースが多いです。
幸いにも、
esm-integration という提案(既にバンドラーで実装済み、Firefox でも積極的に取り入れられている)が、JS モジュールシステムを使って WebAssembly モジュールをインポートできるようにします。
import { run } from "/module.wasm"; run();
また
type="module" を指定した <script> タグから直接ロードすることも可能です。
<script type="module" src="/module.wasm"></script>
これにより、WebAssembly モジュールのロードとインスタンス化で最も一般的なパターンが簡素化されます。しかし、この解決策は初期の難しさを和らげるだけで、本質的な問題には直結していません。
Web API の使用
JavaScript から Web API を呼び出すのは次のように非常にシンプルです:
console.log("hello, world");
WebAssembly では状況が格段に複雑になります。WebAssembly は Web API に直接アクセスできず、JavaScript を介さなければなりません。
単行
console.log の例を示します。
JavaScript 側
// Wasm コードの生メモリへアクセスするため、ここで作成してインポートとして渡す。 let memory = new WebAssembly.Memory(/* ... */); function consoleLog(messageStartIndex, messageLength) { // メッセージは Wasm のメモリに格納されているが、 // JS 文字列へデコードする必要がある(DOM API が要求)。 let messageMemoryView = new Uint8Array( memory.buffer, messageStartIndex, messageLength); let messageString = new TextDecoder().decode(messageMemoryView); // Wasm は `console` グローバルにアクセスできないため、ここで行う。 return console.log(messageString); } // インポートとして Web API を渡す。 let imports = { "env": { "memory": memory, "consoleLog": consoleLog, }, }; let { instance } = await WebAssembly.instantiateStreaming(bytecode, imports); instance.exports.run();
WebAssembly 側
(module ;; JS コードからメモリをインポート (import "env" "memory" (memory 0)) ;; JS consoleLog ラッパー関数をインポート (import "env" "consoleLog" (func $consoleLog (param i32 i32)) ) ;; run 関数をエクスポート (func (export "run") (local i32 $messageStartIndex) (local i32 $messageLength) ;; Wasm メモリに文字列を作成し、ローカルへ格納 ... ;; consoleLog を呼び出す local.get $messageStartIndex local.get $messageLength call $consoleLog ) )
このようなコードは「バインディング」または「グリューコード」と呼ばれ、ソース言語(C++、Rust など)と Web API の橋渡しをします。データのエンコーディング/デコードを行うため、非常に面倒で定型的です。そのため
embind や wasm-bindgen といったツールで自動生成されるケースが多いです。
ただし、このグリューコードは実行時コストも伴います。JavaScript オブジェクトの確保とガベージコレクション、文字列の再エンコード、構造体のデシリアライズなどが発生します。この境界でのオーバーヘッドは、呼び出し自体が高速でも存在します。
「Wasm が DOM サポートをいつ持つか?」という質問は、このようなグリューコードに依存しているためです。実際には WebAPI にアクセスできるものの、JavaScript の橋渡しが必要です。
それが重要なのはなぜ?
技術的には現状でも動作します。WebAssembly はウェブ上で稼働し、多くのプロジェクトが成功裏にデリバリーされています。
しかし平均的なウェブ開発者にとっては、現状は不十分です。WebAssembly を使うこと自体が複雑すぎて、二級体験を避けたくなるケースが多いです。結果として WebAssembly は「パワーユーザー向け機能」になり、平均的な開発者は使わない傾向にあります。
一般的な開発者の旅路
JavaScript
プロジェクトの規模が増すにつれて、徐々に複雑な機能を学んでいく滑らかな曲線があります。
WebAssembly
すぐに「壁」を越える必要があり、多くの要素をまとめて動かさないといけません。結果として大規模プロジェクト向きになりやすいです。
なぜこのようになるのか?
主な理由は以下で、全て WebAssembly がウェブ上で二級言語であることに起因します。
-
コンパイラが一次体験を提供しづらい
ウェブをターゲットする言語は、Wasm ファイルだけでなく、JS 伴奏ファイル(ロード・WebAPI アクセス・その他の処理)も生成する必要があります。これは各言語ごとに再実装が必要で、非ウェブプラットフォームでは再利用できません。 -
標準コンパイラはウェブ向け WebAssembly を生成しない
Clang/LLVM などの上流コンパイラは JS やウェブプラットフォームを意識せず、すべてのプラットフォームで動く単一バイナリを生成したいだけです。そのため、WebAssembly 用の「公式」ツールチェーンが不足しています。 -
ウェブドキュメントは JavaScript 向け
ほとんどの資料が JavaScript 開発者向けに書かれています。JavaScript を知らない場合、Web API の使い方を理解するのが非常に難しくなります。 -
Web API 呼び出しは遅いこともある
1 回でさえ多くのオーバーヘッドがあります。エンジン側は最適化していますが、問題は残ります。すべてのワークロードに影響するわけではありませんが、注意が必要です。console.log -
常に JavaScript レイヤーを理解しなければならない
各言語は自前でウェブプラットフォームへの抽象化を構築します。この抽象化はリークしており、真剣に WebAssembly を使うと最終的には JavaScript を直接読む・書く必要があります。
どうすれば解決できるか?
これは技術的にも社会的にも複雑な問題で、一つの解決策だけでは不十分です。まず「理想的に何があれば助けになるだろう?」と問ってみます。
- 標準化された自己完結型実行アーティファクト
- 複数言語・ツールチェーンでサポートされる
- WebAssembly コードのロード・リンクを担う
- Web API の使用もサポートする
そのようなものがあれば、言語はこのアーティファクトを生成し、ブラウザは JavaScript を介さずに実行できます。標準上流コンパイラやランタイム、ツールチェーン、人気のあるパッケージでサポートされる可能性があります。
WebAssembly コンポーネントモデル
WebAssembly コンポーネントモデルはこれらを目指す提案であり、長年にわたり開発が進められています。高レベル API を低レベル WebAssembly コードの束として実装します。
現時点でできること
- 複数言語からコンポーネントを作成
- ブラウザ(多くはポリフィル)やその他ランタイムで実行
- コンポーネント間でリンクし、コード再利用を可能に
- WebAssembly コードが直接 Web API を呼び出せる
詳細は Component Book や「What is a Component?」をご覧ください。
例:コンポーネントで console.log
console.log以下は実装の一例(チュートリアルではありません)。
- WIT(WebAssembly Interface Types)で必要な API を宣言
component { import std:web/console; }
- Rust でインポートを利用
use std::web::console; fn main() { console::log("hello, world"); }
- ブラウザにコンポーネントを読み込む
<script type="module" src="component.wasm"></script>
ブラウザは自動的にコンポーネントをロードし、ネイティブ Web API を直接バインドして実行します。
ハイブリッドアプリケーション
WebAssembly の利用は多くの場合 JavaScript と併用されるハイブリッドアプリです。コンポーネントはクロス言語相互運用をサポートすることでこの課題にも対処します。
例:画像デコーダーコンポーネント
interface image-lib { record pixel { r: u8; g: u8; b: u8; a: u8; } resource image { from-stream: static async func(bytes: stream<u8>) -> result<image>; get: func(x: u32, y: u32) -> pixel; } } component { export image-lib; }
任意の言語で実装したコンポーネントは、JavaScript から次のように利用できます。
import { Image } from "image-lib.wasm"; let byteStream = (await fetch("/image.file")).body; let image = await Image.fromStream(byteStream); let pixel = image.get(0, 0); console.log(pixel); // { r: 255, g: 255, b: 0, a: 255 }
次のステップ
WebAssembly コンポーネントは、ウェブ上で WebAssembly に一次体験を与える有望な進歩です。Mozilla は WebAssembly CG と協力してコンポーネントモデルを設計中であり、Google も評価しています。
試したい方は以下から始めてください:
- 最初のコンポーネントをビルドする方法を学ぶ
- Jco(ブラウザ)や Wasmtime(CLI)でテストする
- フィードバックやコードを貢献し、ツールはまだ発展途上です
開発中の仕様は component-model プロポーザルリポジトリにあります。
WebAssembly は 2017 年以降大きく進歩しました。今後も「パワーユーザー向け機能」から平均的な開発者が恩恵を受けられるものへと変わる可能性があります。