WebAssembly

ブラウザーでネイティブに近い速度でコードを実行するバイナリ命令形式

Webパフォーマンス
「WebAssembly」の技術書を見る →

WebAssembly とは

WebAssembly (Wasm) は、ブラウザーでネイティブに近い速度でコードを実行するバイナリ命令形式である。C、C++、Rust などのコードを .wasm にコンパイルし、JavaScript から呼び出せる。2017 年に主要ブラウザーの対応が揃い、中核仕様は 2019 年 12 月 5 日に W3C 勧告 (WebAssembly Core Specification 1.0) となった。後継の 2.0 は SIMD や複数戻り値などの拡張を取り込んだ版だが、2026 年 8 月時点では勧告候補の段階にある。

実行モデルはスタックマシンとして定義されている。各命令はオペランドをスタックに積み、積まれた値を取り出して計算し、結果をスタックへ戻す。これは意味を定めるための定義で、実際のブラウザーは受け取った命令列を検証したうえで、その環境のマシンコードへコンパイルして走らせる。モジュールが使えるメモリは線形メモリと呼ばれる 1 本のバイト列で、JavaScript 側からは伸長可能な ArrayBuffer として見える。ポインターは配列の添字に相当し、モジュールはこの範囲の外へ手を伸ばせない。この閉じ込めが Wasm のサンドボックスの土台になっている。

JavaScript vs WebAssembly

JavaScript と WebAssembly は同じエンジンに載るが、得意な仕事が違う。

観点JavaScriptWebAssembly
実行速度JIT で高速化ネイティブに近い
起動速度パース + JITデコード + コンパイル
型動的型付け静的型付け
メモリGC既定では手動 (線形メモリ)
用途UI、ビジネスロジック計算集約的な処理

実行速度の欄には条件が付く。ネイティブに近い速度が出るのは計算が Wasm の内側で完結している場合で、後述する境界越えが多い使い方では JavaScript との差はほとんど消える。

メモリの欄は 2023 年以降で事情が変わった。Wasm には長く GC がなく、Rust の所有権や C の malloc と free のようにモジュール側でメモリを管理する必要があった。GC を言語機能として持つ Java や Kotlin を Wasm へ載せるには GC の実装ごと .wasm に同梱するしかなく、モジュールが膨らんだ。この制約を緩めたのが WasmGC で、GC の仕組み自体を Wasm 側の機能として持つ拡張である。Chrome と Edge は 119、Firefox は 120 で既定有効になった。2026 年 8 月時点では、GC を持つ言語を Wasm へ移すとき、対象ブラウザーがこの拡張に対応しているかどうかで選べるツールチェーンが変わる。

Rust → Wasm

Rust は Wasm 向けのツールチェーンが最も整っている言語の 1 つで、wasm-pack が .wasm と JavaScript 側のグルーコードをまとめて生成する。#[wasm_bindgen] を付けた関数が JavaScript から呼べる形で書き出される。言語自体の特徴は Rust を参照。

// lib.rs
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
    match n {
        0 | 1 => n,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}
wasm-pack build --target web

JavaScript からの呼び出し

生成された JavaScript 側の入口は非同期になる。.wasm の取得とコンパイルを待つ必要があるためで、初期化の完了前に関数を呼ぶと失敗する。

import init, { fibonacci } from './pkg/my_wasm.js';

await init(); // Wasm モジュールの初期化
const result = fibonacci(40); // ネイティブに近い速度で実行

境界を越えて直接渡せるのは整数と浮動小数点数だけである。文字列や配列、オブジェクトを渡すときは、値を線形メモリへバイト列として書き込み、その位置と長さを数値として受け渡す。wasm-bindgen が生成するグルーコードはこの詰め替えを自動で行うが、コピーそのものは消えない。1 回あたりの計算量が小さいのに境界を何度も往復する使い方では、詰め替えの費用が計算の節約分を食い潰す。引数と戻り値が数値だけの fibonacci のような関数が例に使われるのは、この費用が最小になる形だからである。

ユースケース

WebAssembly は JavaScript では処理速度が不足する計算集約型の処理に適している。C/C++/Rust で書かれた既存のライブラリをブラウザーで実行できるため、ネイティブアプリの Web 移植にも活用される。

ユースケース例
画像 / 動画処理Photoshop Web, FFmpeg.wasm
ゲームUnity の WebGL ビルド
暗号化ハッシュ計算、暗号化処理
データ処理CSV パース、圧縮 / 展開
CAD / 3DAutoCAD Web

ゲームエンジンの対応状況はエンジンごとに異なる。Unreal Engine は 4.24 でブラウザー向けの書き出しを本体から外し、コミュニティが保守する拡張へ移した。ブラウザー配信を前提にするなら、エンジン側が現在どこまで面倒を見るかを先に確認する必要がある。

WASI (WebAssembly System Interface)

ブラウザーの中の Wasm はファイルもソケットも触れない。ホストから渡された関数しか呼べないためで、ブラウザーの外で Wasm を動かすには、ファイルやネットワークを扱う標準の入口が別に必要になる。それを定めているのが WASI である。

ブラウザ外での Wasm 実行:
  Wasm → WASI → ファイルシステム、ネットワーク

用途:
  - サーバーサイド・エッジでの実行
  - エッジコンピューティング
  - プラグインシステム

WASI には 0.1、0.2、0.3 の区切りがあり (それぞれ Preview 1、2、3 とも呼ばれる)、0.2 以降はコンポーネントモデルという別の仕様の上に組み直されている。0.3 では非同期処理が仕様側に取り込まれた。実装は wasmtime、WasmEdge、wazero、Wasmer など複数あり、どの版まで対応しているかはランタイムごとに異なる (2026 年 8 月時点)。権限の扱いは能力ベースで、モジュールは何の権限も持たない状態から始まり、ホストが明示的に渡したファイルやソケットだけを使える。

Lambda での Wasm

Node.js のランタイムにも WebAssembly があるため、Lambda の中で .wasm を読み込んで呼び出せる。

import { readFileSync } from 'fs';

// 初期化はモジュール読み込み時の 1 回だけ行う
const wasmBuffer = readFileSync('./optimized.wasm');
const { instance } = await WebAssembly.instantiate(wasmBuffer);
const heavyComputation = instance.exports.heavyComputation as (n: number) => number;

export const handler = async (event: { value: number }) => {
  return { result: heavyComputation(event.value) };
};

モジュールの読み込みとコンパイルはハンドラーの外で 1 回だけ行い、実行環境が再利用される間は使い回す。ハンドラーの中で毎回 instantiate すると、その時間が呼び出しごとに乗る。速くなるかどうかは処理の中身で決まる。純粋な数値計算で、C や Rust の実装が既にあるなら移す価値がある。一方で JavaScript のままでも JIT コンパイル が十分に効く処理では、境界越えと初期化の分だけ遅くなることもある。

Wasm の制限

Wasm は JavaScript の置き換えではない。次の制限は仕様の側でそうなっているもので、外すには JavaScript 側の助けが要る。

制限説明
DOM アクセス直接アクセス不可 (JS 経由)
GC既定ではモジュール側で管理
デバッグソースマップが限定的
バンドルサイズ.wasm ファイルの追加

導入の判断は、処理が計算に偏っているか、境界を越える回数を少なく抑えられるかの 2 点でほぼ決まる。UI の組み立てや DOM 操作が主体の部分は JavaScript のままにし、計算の塊だけを Wasm へ切り出すのが扱いやすい。主スレッドを塞ぐ長さの計算なら、Web Worker へ載せる設計も併せて検討する。

共有:

この記事は役に立ちましたか?

関連用語

関連する記事