WebAssembly(Wasm)とは?JavaScript連携とMemory・Table操作の実装

WebAssembly(Wasm)は、ブラウザやサーバー上で実行できるバイナリ形式の命令フォーマットです。C・C++・Rustなどでビルドしたコードをそのままの計算速度に近い形でWeb上へ持ち込むために設計されました。ただし、JavaScriptから実際に触る部分は仕様書どおりに書いても動かないことがあります。この記事では定義と適用範囲を整理したうえで、つまずきの多いWebAssembly.MemoryWebAssembly.Tableの挙動を、Node v26.5.0(V8 14.6)で実行した結果とともに示します。

まとめ:WebAssembly(Wasm)の要点と実装前に押さえる3つの落とし穴

WebAssemblyはJavaScriptの置き換えではなく、数値計算や既存ネイティブ資産の移植を引き受ける補完技術です。DOM操作やネットワークアクセスは今もJavaScript側が担い、Wasmはインポート経由でそれらを呼びます。ブラウザ対応は2017年に主要4ブラウザで出揃っており、対応可否そのものは判断材料になりません。

実装で問題になるのは仕様の細部です。実行して確認した限り、詰まりやすいのは次の3点でした。WebAssembly.Memoryinitialはバイトではなく64KiBページ単位であること、memory.grow()を呼ぶと既存のTypedArrayがデタッチされて読めなくなること、そしてanyfuncのテーブルには素のJavaScript関数を入れられずTypeErrorになることです。以降で、それぞれの実測値と回避方法を順に扱います。

WebAssembly(Wasm)の定義とJavaScriptとの役割分担

WebAssemblyは、W3CのWebAssembly Working Groupが標準化しているバイナリ命令フォーマットです。ソースコードをテキストのまま送るJavaScriptと違い、コンパイル済みのバイナリを配信するため、テキストの構文解析が不要な分だけ読み込みからコンパイル開始までのコストが小さくなります。実行モデルはスタックマシンで、命令はオペランドスタックを介して値をやり取りします。

名前が似ているため混同されがちですが、.NET(ネット)のアセンブリ(.dll.exe)とは無関係の別技術です。日本語では「Webアセンブリ」と表記されることもあり、こちらはWebAssemblyを指します。

JavaScriptを置き換えない設計:DOM操作はホスト経由

WebAssemblyの仕様には、DOMもネットワークAPIもファイルシステムも含まれていません。モジュールが外の世界に触れる手段は、インスタンス化時に渡すインポートオブジェクトだけです。ボタンのクリックを拾うのもfetchで通信するのも、JavaScript側に書いた関数をWasmへ渡して呼んでもらう形になります。

これは制限ではなくサンドボックスの土台です。ホストが渡していない機能にはモジュール側から到達できないため、信頼できないコードを実行する用途でも被害範囲を限定できます。ブラウザ外でファイルやソケットを扱う場合も、この「ホストが明示的に渡す」原則は変わりません。

WebAssemblyでできること・できないことの境界

WebAssemblyは万能ではありません。得意な処理と、仕様上そもそもできない処理を分けて示します。

得意な領域:数値計算と既存C・C++・Rust資産の移植

効果が出るのは、まとまった計算をJavaScript側との往復なしに回せる処理です。画像・音声のエンコードとデコード、物理演算、暗号処理、圧縮、CADやゲームエンジンのような大規模なネイティブコードの移植が代表例です。データベースをブラウザ内で動かす例もあり、DuckDB WASMとは?ブラウザでSQL分析を実行する仕組みと実装・採用判断で扱っているDuckDBは、分析エンジンをまるごとWasm化してブラウザ上でSQLを実行します。

逆に、既存のC言語資産があること自体が採用の強い理由になります。ゼロから書き起こすなら、多くの業務ロジックはJavaScriptのままで十分です。

できないこと:DOM直接操作と任意のシステムコール

Wasmからdocument.getElementByIdを直接呼ぶことはできません。DOMを触るにはJavaScript側のラッパー関数をインポートするか、Emscriptenやwasm-bindgenが生成するグルーコードを経由します。同様に、ブラウザ上のWasmには任意のシステムコールもスレッドの自由な生成もありません。共有メモリとアトミック命令を使ったマルチスレッドは可能ですが、Chrome 74・Firefox 79・Safari 15.2以降という前提条件が付き、さらにサーバー側でSharedArrayBufferを許可するCOOPとCOEPのヘッダー設定が必要です。

採用を見送るべき条件

次のいずれかに当てはまるなら、Wasm化は費用に見合いません。処理時間の大半がネットワーク待ちやDOM更新である場合、JavaScriptとWasmの境界を1フレームに何百回もまたぐ設計になる場合、そして扱うデータが文字列中心の場合です。3つ目は特に見落とされます。WasmのリニアメモリにはJavaScriptの文字列がそのまま置けないため、境界をまたぐたびにUTF-8への変換とコピーが発生し、計算で稼いだ時間を打ち消します。

迷ったら、まずJavaScript版をプロファイラで計測してください。ホットスポットが単一の数値計算ループに収束していないなら、Wasmに置き換えても速くなりません。

JavaScriptからWasmモジュールを読み込む方法とインポートの渡し方

読み込みAPIは4つあります。WebAssembly.instantiateStreamingWebAssembly.instantiateWebAssembly.compileStreamingWebAssembly.compileです。使い分けの基準は単純で、HTTPレスポンスから直接読むならinstantiateStreaming、すでにバイト列を手元に持っているならinstantiateを選びます。compile系はモジュールを一度だけコンパイルして複数回インスタンス化したいときに使います。

instantiateStreamingを既定にする条件

instantiateStreamingはダウンロードとコンパイルを並行させるため、起動が最も速くなります。ただし条件が1つあり、サーバーがContent-Type: application/wasmを返していないと失敗します。静的ホスティングでこのMIMEタイプが設定されていないケースは珍しくないため、配信設定を先に確認してください。

const { instance } = await WebAssembly.instantiateStreaming(
  fetch("/assets/calc.wasm"),
  { env: { log: (v) => console.log("wasm から:", v) } }
);
console.log(instance.exports.double(21));

対応状況はinstantiateStreamingがChrome 60・Firefox 58・Safari 15以降です。Safari 14以前も想定するなら、fetchしてarrayBuffer()を取り、WebAssembly.instantiateに渡す形にフォールバックします。

インポートオブジェクトの過不足で出るTypeError

第2引数のインポートオブジェクトは、モジュールが宣言したインポートをすべて満たす必要があります。上の例でenvを渡し忘れて空オブジェクトを与えたところ、次のエラーで停止しました。

TypeError: WebAssembly.instantiate(): Import #0 "env": module is not an object or function

エラーメッセージにはインポート番号とモジュール名が入ります。実際にenv.logを渡してdouble(21)を呼ぶと、JavaScript側のlogが引数21を受け取り、戻り値42が返りました。インポートは「Wasmが外の世界へ出るための唯一の穴」であり、ここに何を渡すかがそのままモジュールの権限になります。

WebAssembly.Memoryの実装:ページ単位の指定とgrow時のデタッチ

WebAssembly.MemoryはWasmのリニアメモリをJavaScriptから扱うためのオブジェクトです。実体は連続したバイト列で、memory.bufferからArrayBufferとして取り出し、TypedArrayのビューを被せて読み書きします。

initialとmaximumの単位は64KiBページ

コンストラクタに渡すinitialmaximumは、バイト数ではなくWebAssemblyページ数です。1ページは65,536バイト(64KiB)と仕様で固定されています。よく見かける{ initial: 256 }は256バイトではなく、16MiB(16,777,216バイト)を確保する指定です。

const memory = new WebAssembly.Memory({ initial: 256, maximum: 512 });
console.log(memory.buffer.byteLength);
// 16777216  (= 256 * 65536)

実行すると16,777,216バイトが返りました。初期値を数百に置くと十数MB分のアドレス空間が予約される計算になるため、小さなモジュールではinitialを1桁に抑え、必要になってから伸ばす方が無駄がありません。

grow後に既存のTypedArrayが無効化される問題

memory.grow(n)はnページ分メモリを拡張し、戻り値として拡張前のページ数を返します。ここで問題になるのは、拡張の際にmemory.bufferが差し替えられ、それまでに作ったTypedArrayがデタッチされる点です。実測した結果が次のとおりです。

const memory = new WebAssembly.Memory({ initial: 256, maximum: 512 });
const array = new Uint8Array(memory.buffer);
array[0] = 42;

const old = memory.grow(1);
console.log(old);                // 256  (拡張前のページ数)
console.log(array.byteLength);   // 0     (デタッチ済み)
console.log(array[0]);           // undefined

const fresh = new Uint8Array(memory.buffer);
console.log(fresh[0]);           // 42    (データ自体は保持されている)

データが消えたわけではなく、古いビューが無効になっただけです。それでも、Uint8Arrayをモジュールスコープに保持して使い回す実装では、Wasm側が内部でmemory.growを呼んだ瞬間に全ての読み書きが黙って壊れます。ビューは長期保持せず、アクセスのたびにmemory.bufferから作り直すのが安全な書き方です。

この面倒を根本から避ける手段として、memory.toResizableBuffer()が追加されました。返るのはリサイズ可能なArrayBufferで、growしてもビューがデタッチされずに追従します。maximum: 4で作ったメモリで確認すると、maxByteLengthは262,144、grow(1)後もビューのbyteLengthが65,536から131,072へ伸び、書き込んだ値も読めたままでした。対応はChrome 144・Firefox 145・Safari 26.2以降と新しく、一般公開サイトでは母集団が足りません。対象ブラウザを固定できる社内システムなら既定にしてよく、公開サイトではビューを作り直す従来方式をフォールバックとして残してください。

Wasm 3.0のMemory64:address指定とBigInt必須

2025年9月17日に完成したWebAssembly 3.0で、メモリとテーブルのアドレス型にi64を選べるようになりました。従来の4GB上限が外れ、理論上16EBまで扱えます。JavaScript API側では、ディスクリプタにaddress: "i64"を指定します。

// ページ数は BigInt で渡す
const mem64 = new WebAssembly.Memory({ initial: 1n, address: "i64" });
console.log(mem64.grow(1n));  // 1n  (BigInt が返る)

// 数値のまま渡すと失敗する
new WebAssembly.Memory({ initial: 1, address: "i64" });
// TypeError: Cannot convert 1 to a BigInt

注意したいのが、提案段階で使われていたindex: "i64"という古いキーです。これを書いても例外は出ませんが、未知のプロパティとして無視され、ただの32ビットメモリが生成されます。判別方法はgrow()の戻り値で、BigIntが返れば64ビット、通常の数値が返れば32ビットです。古い記事のコードをそのまま持ち込むと、Memory64を使っているつもりで使えていない状態になります。

WebAssembly.Tableの実装:anyfuncテーブルの型制約とexternrefの使い分け

WebAssembly.Tableは関数参照や外部参照を格納する配列に似たオブジェクトで、関数ポインタ相当の動的な呼び出しを実現します。elementに指定できる値は"anyfunc""externref"の2種類で、どちらを選ぶかで格納できるものが変わります。

setが受け付ける値:エクスポート済みWasm関数とnull

anyfuncのテーブルにJavaScriptのアロー関数を入れる例をWeb上でよく見かけますが、これは実行できません。実際に試すと次のTypeErrorになります。

// bytes は (func (export "f") (result i32) i32.const 7) をコンパイルした .wasm
const { instance } = await WebAssembly.instantiate(bytes, {});
const table = new WebAssembly.Table({ initial: 2, element: "anyfunc" });

try {
  table.set(0, () => console.log("Hello from JS!"));
} catch (e) {
  console.log(e.message);
  // WebAssembly.Table.set(): Argument 1 is invalid for table:
  // function-typed object must be null (if nullable) or a Wasm function object
}

// エクスポートされた Wasm 関数なら格納できる
table.set(0, instance.exports.f);
console.log(table.get(0)());  // 7

格納できるのはinstance.exportsから取り出したエクスポート済みWasm関数か、nullだけです。テーブルの初期状態はnullで埋まっており、table.get(0)nullを返します。呼び出し前にnullチェックを入れておかないと、初期化漏れがTypeErrorとして実行時に噴出します。

externrefテーブルでのJavaScript値の保持

JavaScript側のオブジェクトや関数をテーブルに置きたい場合はexternrefを使います。こちらは型の制約がなく、素のアロー関数を格納して取り出し、そのまま呼び出せました。

const t = new WebAssembly.Table({ initial: 1, element: "externref" });
t.set(0, () => "plain js");
console.log(t.get(0)());  // "plain js"

参照型(reference types)はChrome 96・Firefox 79・Safari 15.1以降で使えます。JavaScriptのコールバックを束ねて管理したいだけなら、無理にanyfuncで組まずexternrefを選ぶ方が実装が短くなります。

grow時の挙動と範囲外アクセスのRangeError

テーブルはtable.grow(n)で拡張でき、table.lengthで現在の要素数を取得します。メモリと違い、テーブルの拡張では既存の参照が無効化されません。一方、範囲外のインデックスにアクセスすると例外になります。

const table = new WebAssembly.Table({ initial: 2, element: "anyfunc" });
table.get(5);
// RangeError: WebAssembly.Table.get(): invalid address 5 in funcref table of size 2

エラーメッセージがfuncref tableと表示される点も覚えておくと調査が速くなります。anyfuncは仕様上funcrefの別名であり、エンジン側は内部名のfuncrefで報告します。

C言語・RustからWasmを生成する手順の選び方

ビルド経路は言語ごとに定番が決まっています。C・C++はEmscripten(最新版は6.0.6、2026年8月5日リリース)を使い、emcc main.c -o main.htmlのようにコンパイルすると、.wasmと読み込み用のJavaScriptグルーコード、確認用HTMLがまとめて出力されます。JavaScript側の面倒を見てほしいならこの形が最短です。

Rustではwasm-pack(0.15.0)が標準的な選択肢で、wasm-bindgen(0.2.127)と組み合わせて型変換のグルーコードを自動生成します。ビルドの具体的なコマンドと、出力サイズを削るための設定はRustでWebAssemblyを実装する手順|wasm-packでのWASM化と軽量化・配置先の選び方【2026年版】にまとめています。C#・.NETの場合はBlazor WebAssemblyが同じ役割を担い、採用可否の判断材料はBlazorの将来性は?2026年の採用判断と.NET 10時代のWeb開発を解説で整理しました。

どの経路でも、生成物は.wasmのバイナリと、それを読み込むJavaScriptの2点セットになります。配信時はContent-Type: application/wasmを返す設定と、バイナリへの長期キャッシュ設定を忘れないでください。

ブラウザ対応状況とWasm 3.0機能の実装差

WebAssembly本体の対応は完了済みで、Chrome 57・Firefox 52・Safari 11・Edge 16以降で動きます。Node.jsは8.0.0からです。判断が必要なのは本体ではなく、Wasm 3.0でまとまった各機能の実装状況です。

機能別の対応バージョン

機能 Chrome Firefox Safari
WebAssembly 本体 57 52 11
例外処理(旧提案・Legacy) 95 100 15.2
例外処理(exnref・Wasm 3.0版) 137 131 18.4
末尾呼び出し 112 121 18.2
ガベージコレクション 119 120 18.2
型付き関数参照 119 120 18
複数メモリ 120 125 未対応
64ビットアドレス (Memory64) 133 134 未対応
緩和SIMD 114 146 未対応
JSPI 137 153 27(ベータ)
Memory.toResizableBuffer 144 145 26.2

数値はMDNのブラウザ互換データ(@mdn/browser-compat-data v8.0.10、2026年8月時点)から取得したものです。表を読むときに外せない注意点が2つあります。1つは例外処理が2行に分かれていること。Wasm 3.0が標準化したのはexnrefを使う最終版で、Safariでは18.4(2025年3月)が下限です。広く引用される15.2という数値は旧提案(Legacy)のもので、3.0準拠の実装とは別物です。もう1つはJSPIのSafari 27が2026年8月時点でベータであること。現行リリースは26.6のため、まだ出荷済みとして扱えません。緩和SIMDもChrome 114(2023年5月)からFirefox 146(2025年12月)まで3年半の開きがありました。

結論として、Safariは複数メモリ・Memory64・緩和SIMDの3つが未対応です。iOSではSafariのエンジンが事実上の共通基盤になるため、これら3機能に依存したバイナリを配信するとiOS環境で動きません。汎用のWebサイトで採用してよいのは、GC・型付き参照・末尾呼び出しまでと考えてください。

about:configのwasm系設定に関する訂正

Firefoxでjavascript.options.wasmtrueにすればWasmが有効化される、という説明が古い記事に残っています。現行のFirefoxソース(StaticPrefList.yaml)を確認したところ、この名前の設定項目は存在しませんでした。存在するのはjavascript.options.wasm_で始まる29件の個別設定で、wasm_cachingwasm_optimizingjitwasm_relaxed_simdなどのように機能ごとに分かれています。

WebAssemblyそのものは既定で有効なので、通常の利用でabout:configを触る必要はありません。検索でよく参照されるjavascript.options.wasm_js_promise_integrationもソース上の既定値はtrueで、JSPIは設定変更なしで使えます。デバッグはabout:configではなくDevToolsが担当で、ChromeもFirefoxもWasmのソースビューとブレークポイント、コールスタックの確認に対応しています。DWARFの情報を含めてビルドしておけば、元のC言語やRustのソース行を追いながらステップ実行できます。

ブラウザ外・エッジでWasmを動かす選択肢

WebAssemblyの利用先はブラウザに限りません。同じバイナリをサーバーやエッジで動かす用途が広がっており、その入口になるのがWASIとは|WebAssemblyをブラウザ外で動かすシステムインターフェース【2026年版】で扱っているシステムインターフェースです。ファイル・時刻・乱数・ソケットといったOS機能を、ホストが明示的に渡す形で標準化しています。

実行環境としては、CNCFプロジェクトの軽量ランタイムを解説したWasmEdgeとは?CNCFの軽量WebAssemblyランタイムの特徴とコンテナとの違い【2026年版】(最新版0.17.1、2026年7月6日リリース)や、Kubernetes上でWasmワークロードを動かすSpinKubeとは?WASMアプリをKubernetesで動かす仕組みと採用判断が選択肢になります。エッジで効くのは配布物の小ささです。今回の検証用に組んだ最小モジュールは、メモリ・インポート・エクスポートを持たせて75バイトでした。実務のモジュールが数百KBに収まるのに対し、同じ処理をコンテナで配ると基底イメージだけで数十MBかかります。

一方、成熟したエコシステムやデバッグ環境はコンテナに分があります。既存のコンテナ基盤が安定して回っているなら、置き換えではなく起動速度が要件になる部分だけに限定して試す方が現実的です。

よくある質問

WebAssemblyは何ができますか?

C・C++・Rustなどでビルドしたコードを、ブラウザやサーバー上でネイティブに近い速度で実行できます。画像や音声のエンコードとデコード、物理演算、暗号処理、圧縮、ゲームエンジンやCADの移植、ブラウザ内でのデータベース実行が代表例です。

WebAssemblyでできないことは?

DOMの直接操作、ネットワークアクセス、ファイル入出力、任意のシステムコールはWasm単体ではできません。いずれもインポートオブジェクト経由でJavaScript側の関数を呼ぶ形になります。

WebAssemblyのデメリットは?

JavaScriptとの境界をまたぐコストが最大の弱点です。文字列やオブジェクトはリニアメモリにそのまま置けず、往復のたびに変換とコピーが発生します。ビルド環境の準備と生成物のサイズ管理も運用コストとして残ります。

WebAssembly.Memoryのinitialに指定する数値の単位は?

バイトではなくWebAssemblyページ数で、1ページは65,536バイト(64KiB)に固定されています。new WebAssembly.Memory({ initial: 256 })とすると16,777,216バイトが確保されます。Node v26.5.0でmemory.buffer.byteLengthを実行して得た値です。数百単位の指定は十数MBの確保を意味します。

WebAssembly 3.0では何が変わりましたか?

2025年9月17日に完成し、64ビットアドレス空間(Memory64)、1モジュール内での複数メモリ、ガベージコレクション、型付き参照、末尾呼び出し、例外処理、緩和SIMDが標準に入りました。ただしSafariは複数メモリ・Memory64・緩和SIMDが未対応です。iOSを含む一般公開向けサイトではGC・型付き参照・末尾呼び出しまでに留め、例外処理はSafari 18.4以降を前提にできる場合に限るのが安全です。3.0版の例外処理はexnrefを使う最終版で、よく引用されるSafari 15.2は旧提案(Legacy)の数値です。

関連記事

資料請求

RELATED POSTS 関連記事