- Node.js
WebAssembly(Wasm)とは|仕組みとJSとの速度を実測
結論
WebAssembly(略称 Wasm)は、ブラウザや Node.js などで動く、コンパクトなバイナリ形式のプログラムの形です。C・C++・Rust などで書いたコードを .wasm にコンパイルし、JavaScript から呼び出して使います。JavaScript を置き換えるものではなく、重い計算を受け持つ相棒です。
この記事でわかること
- 41バイトの .wasm を作り、先頭の4バイトとセクションを読んだ結果
- Node 22 と Chromium 153 で測った JS との速度比較(速い処理・遅い処理)
- WASI でファイルを読む実物と、よくあるエラーの原文と直し方
WebAssembly は「どの CPU でも動く共通の機械語」のようなものです。人が書いたコードを、コンパイラが .wasm に変換し、ブラウザや Node.js の中のエンジンが、その場の CPU の機械語に変換して実行します。
flowchart TD
S[C・C++・Rust・Go<br>WAT(テキスト形式)] --> C[コンパイラ<br>clang・rustc・wat2wasm]
C --> W[.wasm<br>バイナリ形式]
W --> E[実行環境が機械語に変換<br>ブラウザ・Node.js・wasmtime]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| WebAssembly とは | ブラウザ等で動くバイナリ形式の命令セット。W3C の標準 | WebAssembly(Wasm)とは |
| wasm とは | WebAssembly の公式の略称。ファイルの拡張子も .wasm | WebAssembly(Wasm)とは |
| JavaScript より速いのか | 重い計算は手元で約1.6〜2.2倍速かった。小さな関数を何度も呼ぶと遅い | JavaScriptとWasmの速度比較 |
| 何に使われているか | Figma・Photoshop の Web 版・エッジのサーバー(Cloudflare Workers)など | できること・使う場面 |
| どう作るか | C・Rust をコンパイルするか、WAT から wat2wasm で作る | C・Rust・Emscriptenからwasmを作る |
| ブラウザでの読み込み方 | WebAssembly.instantiateStreaming(fetch(...))。MIME は application/wasm |
ブラウザ(Chrome)でwasmを実行する |
| WASI とは | ブラウザの外で、ファイルや標準出力を使うための決まり | WASIとは |
| DOM を操作できるか | 直接はできない。JS の関数を import して呼ぶ | ブラウザ(Chrome)でwasmを実行する |
確認環境:Ubuntu 24.04.5 LTS(2コアの共有環境)で、wabt 1.0.34 の wat2wasm で WAT から、Ubuntu clang 18.1.3(wasm-ld 18。WASI 用は apt の wasi-libc)で C から .wasm を作りました。実行は Node.js 22.23.4、Chromium 153.0.8010.12(playwright 1.63.0。127.0.0.1 の自作サーバーから読み込み)、wasmtime-py 50.0.1 です。Rust(rustc 1.97.0)はエラーの原文だけを確かめました。wasmtime の CLI・Emscripten・各ブラウザの対応版は、WebAssembly 公式サイト・MDN・各社の公開資料で確かめています。図は mermaid 11.16.0 で描画を確かめました(確認日 2026年10月29日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「WebAssembly(Wasm)とは|仕組みとJSとの速度を実測」
WebAssembly(Wasm)とは
この節では、WebAssembly が何で、JavaScript とどういう関係にあるのかが分かります。
WebAssembly は、ブラウザなどで実行できる、コンパクトなバイナリ形式の低レベルな言語です。 MDN は「ネイティブに近いパフォーマンスで動作する、コンパクトなバイナリー形式の低レベルなアセンブリー風言語」と説明し、C・C++・C#・Rust などのコンパイル先になること、JavaScript を補完して並べて動かすように設計されていることを挙げています。略称は Wasm(ワズム)で、WebAssembly の公式サイトもこの書き方を使っています。
バイナリ形式
.wasm は人が読む文字ではなくバイト列。小さく、読み込みと変換が速い。
先頭は 00 61 73 6d
CPU に依存しない
x86 でも Arm でも同じ .wasm が動く。実行する側が、その場の機械語に変換する。
スタックマシン型
サンドボックス
.wasm は、渡された関数とメモリしか触れない。ファイルや DOM は、外から渡さない限り使えない。
import と export
Web の標準
2019年12月5日に W3C 勧告。主要なブラウザが対応し、2025年9月17日に Wasm 3.0 が標準になった。
W3C・WebAssembly CG
名前の意味と、よくある勘違い
「Web」で使える「Assembly(アセンブリ言語のような低レベルな命令)」という意味の名前です。ただし、Web 専用ではありません。web.dev の解説も、WASI(後の節)を使えばブラウザの外でも動くことを紹介しています。逆に、手で .wasm を書いて Web サイトを作る技術でもありません。多くの場合、ほかの言語で書いたコードの「コンパイル先」です。
JavaScript との関係
見分ける問い:「JS で遅い処理か、既存の C・Rust のコードを使いたいか」
WebAssemblyの仕組み(コンパイルから実行まで)
この節では、ソースコードが .wasm になり、ブラウザの中で動くまでの流れが分かります。
WebAssembly は、スタックマシン(値を積んで、上から取り出して計算する方式)を前提にした命令の集まりです。 web.dev の解説は、5と10を掛ける例を、x86 のアセンブリ言語と WebAssembly で並べています。WebAssembly では i32.const 5・i32.const 10・i32.mul のように、値を積んでから命令を実行します。
- 1ソースを書くC・C++・Rust・Go・Kotlin など。手で書くならテキスト形式(WAT)
- 2.wasm にコンパイルするclang・rustc・Emscripten・wat2wasm。CPU に依存しないバイト列になる
- 3配信・読み込みブラウザは fetch で受け取り、Content-Type が application/wasm なら受信しながら変換できる
- 4検証とコンパイル(Module)型や命令の並びを検証し、その CPU の機械語に変換する
- 5インスタンス化(Instance)import(JS の関数・メモリ)を渡して、呼べる状態にする
- 6JS から export を呼ぶinstance.exports.add(2, 3) のように、普通の関数として呼ぶ
JavaScript は、ソースを読んで解析し、動かしながら型を推測して最適化(JIT)します。.wasm は、型が最初から決まっていて解析済みの形で届くため、読み込んだ直後から安定した速さで動きやすい、というのが速さの理由です。どれくらい速いかは処理によって変わり、後の節で実測しています。
値の型は4つの数値が基本
関数の引数と戻り値に使える基本の型は、i32・i64(整数)と f32・f64(浮動小数点数)です。文字列や配列の型はありません。文字列はメモリ(線形メモリ)にバイト列として置き、その位置を数値で渡します(線形メモリの節)。後の版で、SIMD 用の v128 や、参照の型(funcref・externref)、GC で使う構造体と配列が加わりました。
JavaScript 側のイベントループと非同期の流れは、関連記事「JavaScriptイベントループ|マイクロタスクの実行順を図とクイズで」で扱っています。.wasm の関数呼び出しは同期で、呼んでいる間はほかの処理が止まります。
WebAssemblyでできること・使う場面(判断表)
この節では、WebAssembly が実際にどこで使われ、自分の用途に向くかどうかを判断する目安が分かります。
向いているのは「計算が重い処理」と「すでにある C・C++・Rust の資産を Web で使いたい場合」です。 各社の公開情報から、確かめられたものを挙げます。
1Figma(デザインツール)
C++ で書かれた製品を WebAssembly に切り替え、読み込み時間が文書の大きさによらず3倍以上速くなった、と Figma の公式ブログが書いている。
2Photoshop の Web 版
既存の C++ のコードを Emscripten で WebAssembly にした。web.dev の記事は、スレッド・C++ の例外・SIMD を使い、SIMD で平均3〜4倍速くなったと紹介している。
3エッジのサーバー
Cloudflare Workers は Wasm に対応する。公式ドキュメントは、WASI の対応は実験的で一部のシステムコールだけ、と説明している。
4ほかの言語を Web で動かす
Wasm 3.0 の GC により、Java・Kotlin・Scala・OCaml・Dart などが Wasm を出力先にしている(WebAssembly 公式のお知らせ)。
そのほか、画像・音声・動画の処理、ゲームエンジン、物理演算、圧縮・暗号、PDF の描画、ブラウザの中で動く SQLite やプログラミング言語の実行環境などが、典型的な使い道です。
使うかどうかの判断表
| やりたいこと | 向き不向き | 理由 |
|---|---|---|
| 画像・動画・音声の変換、圧縮 | 向く | 大きな配列をまとめて計算する。C・Rust のライブラリが使える |
| C・C++・Rust の既存ライブラリを Web で使う | 向く | 書き直さずにコンパイルできる(Figma・Photoshop の例) |
| ゲーム・3D・シミュレーション | 向く | 描画は WebGL・WebGPU、計算は Wasm と分ける |
| プラグインを安全に動かす(サーバー側) | 向く | 渡した機能しか使えないサンドボックス。WASI で権限を絞れる |
| フォームの入力チェック・画面の切り替え | 向かない | DOM を直接触れない。JS のほうが速く簡単 |
| 小さな関数を JS から何万回も呼ぶ | 向かない | JS と Wasm の行き来に時間がかかる(手元で約9〜15倍遅い) |
| サーバーの API(データベース中心) | 多くは不要 | 待ち時間の大半は通信と DB。Node・Go・Java で足りることが多い |
最小のwasmを作る(WATとwat2wasm)
この節では、テキスト形式(WAT)で関数を1つ書き、.wasm にする手順と出力が分かります。
WAT(WebAssembly Text Format)は、.wasm と1対1に対応する、人が読み書きできる形です。 S 式(かっこで囲む書き方)で書きます。2つの整数を足す関数 add を書きました。
(module
(func $add (export "add") (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add))
local.get で引数を2つ積み、i32.add が2つを取り出して足した結果を積みます。最後に積まれている値が戻り値です。wabt(The WebAssembly Binary Toolkit)の wat2wasm で変換しました。
$ sudo apt install wabt
$ wat2wasm --version
1.0.34
$ wat2wasm add.wat -o add.wasm
$ ls -l add.wasm
-rw-r--r-- 1 root root 41 Oct 29 19:16 add.wasm
足し算1つの .wasm は 41バイトでした。逆に wasm2wat で .wasm をテキストに戻せます。名前($a・$b)は消え、番号になりました。
$ wasm2wat add.wasm
(module
(type (;0;) (func (param i32 i32) (result i32)))
(func (;0;) (type 0) (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
(export "add" (func 0)))
Chrome の開発者ツールで .wasm を開いたときに見えるのも、このテキスト形式です。開発者ツールの基本は、関連記事「Chrome DevToolsの使い方|デバッグ手順と便利機能一覧」で扱っています。
.wasmファイルの中身(マジックナンバーとセクション)
この節では、.wasm のバイナリの先頭が何を意味し、どんなセクションでできているのかが、実物のバイト列で分かります。
.wasm の先頭4バイトは必ず 00 61 73 6d(「\0asm」)で、続く4バイトが版(01 00 00 00=1)です。 41バイトの add.wasm を od で表示しました。
$ od -A x -t x1z -v add.wasm
000000 00 61 73 6d 01 00 00 00 01 07 01 60 02 7f 7f 01 >.asm.......`....<
000010 7f 03 02 01 00 07 07 01 03 61 64 64 00 00 0a 09 >.........add....<
000020 01 07 00 20 00 20 01 6a 0b >... . .j.<
000029
| バイト | 意味 | add.wasm での値 |
|---|---|---|
00 61 73 6d |
マジックナンバー(\0asm) | どの .wasm でも同じ |
01 00 00 00 |
バイナリ形式の版 | 1(Wasm 3.0 でも1のまま) |
01(Type) |
関数の型の一覧 | (i32, i32) -> i32 |
03(Function) |
各関数がどの型か | 関数0は型0 |
07(Export) |
外に見せる名前 | "add" → 関数0 |
0a(Code) |
関数の本体(命令) | 20 00 20 01 6a 0b |
7f・6a・0b |
i32 型・i32.add 命令・end | 命令は1バイトのものが多い |
wasm-objdump -h(一覧)・-x(中身)・-d(命令)でセクションを読めます。C から作った calc.wasm(後の節)は、Memory・Global のセクションと、名前やコンパイラの情報を入れた Custom セクション(name・producers・target_features)も持っていました。
バイト列を16進で読む方法そのものは、関連記事「バイナリエディタとは|使い方・おすすめとxxdで読む実例・戻し方」で扱っています。
Node.jsでwasmを実行する
この節では、.wasm を Node.js から読み込んで関数を呼ぶコードと、引数の型の変換で起きることが分かります。
Node.js には、ブラウザと同じ WebAssembly オブジェクトが最初から入っています。 追加のパッケージは要りません。
// run-add.mjs
import { readFile } from 'node:fs/promises';
const bytes = await readFile(new URL('./add.wasm', import.meta.url));
console.log('validate:', WebAssembly.validate(bytes));
const { instance, module } = await WebAssembly.instantiate(bytes);
console.log('exports:', WebAssembly.Module.exports(module));
console.log('add(2, 3) =', instance.exports.add(2, 3));
console.log('add(2147483647, 1) =', instance.exports.add(2147483647, 1));
console.log('add(1.9, 2.9) =', instance.exports.add(1.9, 2.9));
console.log('add("10", 5) =', instance.exports.add("10", 5));
$ node run-add.mjs
validate: true
exports: [ { name: 'add', kind: 'function' } ]
add(2, 3) = 5
add(2147483647, 1) = -2147483648
add(1.9, 2.9) = 3
add("10", 5) = 15
JS の感覚で書くと驚く点
i32 の最大値に1を足すと、マイナスに一周した
1.9 と 2.9 は小数点以下を切り捨てて 1+2=3
文字列 "10" も数値に変換されて通った
理由
i32 は32ビットの整数。桁あふれは折り返す決まり
JS の数値は、引数の型(i32)に変換されてから渡る
型の検査はしないので、JS 側で値を確かめる
i64 の引数には、JS の Number ではなく BigInt を渡します。Number を渡すとエラーになりました(エラーの節)。Node.js 自体の入れ方や版の切り替えは、関連記事「Node.js バージョン管理|nvm・fnm・mise・Volta比較と実測」で扱っています。
ブラウザ(Chrome)でwasmを実行する
この節では、Web ページから .wasm を読み込む書き方と、wasm から DOM を書き換える方法、ブラウザで出たエラーの原文が分かります。
ブラウザでは WebAssembly.instantiateStreaming(fetch('/calc.wasm')) がいちばん効率のよい読み込み方です。 ダウンロードしながらコンパイルできます。127.0.0.1:45727 に Node.js で小さなサーバーを立て、Chromium 153 で開きました。
<p id="out">(まだ)</p>
<script>
(async () => {
// 計算用の wasm
const { instance } = await WebAssembly.instantiateStreaming(fetch('/calc.wasm'));
console.log(instance.exports.fib(20));
// wasm から JS の関数を呼んで DOM を書き換える
const dom = await WebAssembly.instantiateStreaming(fetch('/dom.wasm'), {
env: { setText: v => { document.getElementById('out').textContent = 'wasm から受け取った値: ' + v; } }
});
dom.instance.exports.run(21);
})();
</script>
dom.wasm は、受け取った数を2倍して、import した env.setText を呼ぶだけのモジュールです。
(module
(import "env" "setText" (func $setText (param i32)))
(func (export "run") (param $n i32)
local.get $n
i32.const 2
i32.mul
call $setText))
画面の文字は「wasm から受け取った値: 42」に変わりました。Wasm には DOM を触る命令が無いため、画面を変えるときは、このように JS の関数を import して呼びます。 Rust の wasm-bindgen や Emscripten は、この「つなぎの JS(グルーコード)」を自動で作る道具です。
ブラウザで出たエラー
| 状況 | Chromium 153 のエラー原文 |
|---|---|
サーバーが application/octet-stream で返した |
TypeError: Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'. |
| .wasm が404 | TypeError: Failed to execute 'compile' on 'WebAssembly': HTTP status code is not ok |
| import を渡し忘れた | TypeError: WebAssembly.instantiate(): Imports argument must be present and must be an object |
| HTML を file:// で開いた | Fetch API cannot load file:///calc.wasm. URL scheme "file" is not supported. |
MIME タイプのエラーは MDN にも書かれており、instantiateStreaming を使うには、サーバーが .wasm を application/wasm で返す必要があります。サーバーの設定を変えられないときは、fetch → arrayBuffer() → WebAssembly.instantiate() の順に読み込めば、MIME タイプに関係なく動きます(受信しながらのコンパイルはできません)。fetch の書き方は、関連記事「JavaScript fetchの使い方|用途別コピペ関数と実行結果」で扱っています。
C・Rust・Emscriptenからwasmを作る
この節では、C を clang で .wasm にする手順と、つまずいたエラー、Rust と Emscripten の位置づけが分かります。
clang は、ターゲットに wasm32 を指定するだけで .wasm を出力できます。 Ubuntu 24.04 の clang 18 には wasm32 のターゲットと wasm-ld が入っていました。計算用の関数を4つ書きました(一部)。
int fib(int n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
int add(int a, int b) { return a + b; }
/* ほかに count_primes(エラトステネスのふるい)と mandel(マンデルブロ集合)*/
そのままリンクすると _start が無いと言われる
main の無いライブラリとして作るので、-Wl,--no-entry を付ける。
関数が export されない
--no-entry だけだと export は memory だけで、JS から呼ぶと is not a function。-Wl,--export=fib のように1つずつ書く(--export-all は内部の値まで出る)。
memset が無いと言われる
-O2 で、配列を埋めるループが memset の呼び出しに置き換わった。標準ライブラリ無し(-nostdlib)なので見つからない。-fno-builtin を付けるか、wasi-libc を使う。
stdio.h が無いと言われる
printf などを使うには WASI の C ライブラリ(wasi-libc)が要る。apt の wasi-libc を入れ、--target=wasm32-wasi にする(WASI の節)。
最終的に使ったコマンドです。1,116バイトの calc.wasm ができました。
clang --target=wasm32 -O2 -fno-builtin -nostdlib \
-Wl,--no-entry \
-Wl,--export=fib -Wl,--export=count_primes -Wl,--export=mandel -Wl,--export=add \
-o calc.wasm calc.c
1千万要素の配列(ふるい)を静的に確保したため、メモリの初期サイズは initial=154 ページ(1ページ64KiB、約9.6MiB)になっていました。C 言語そのものは、関連記事「C言語とは|入門から環境構築・ポインタ・mallocまで実行例で解説」で扱っています。
Rust と Emscripten
Rust は wasm のターゲットを持っていますが、標準ライブラリを別に入れます。手元の rustc 1.97.0 では、ターゲットの一覧に wasm32-unknown-unknown・wasm32-wasip1・wasm32-wasip2 などがあり、入れずにコンパイルすると次のエラーになりました。
$ rustc --target wasm32-unknown-unknown --crate-type cdylib -O add.rs
error[E0463]: can't find crate for `std`
|
= note: the `wasm32-unknown-unknown` target may not be installed
= help: consider downloading the target with `rustup target add wasm32-unknown-unknown`
| 道具 | 入力 | 主な用途 |
|---|---|---|
| wat2wasm(wabt) | WAT | 学習・小さなモジュール・テスト |
| clang(LLVM) | C・C++ | JS のつなぎ無しで小さな .wasm を作る |
| Emscripten(emcc) | C・C++ | 既存のアプリの移植。JS のつなぎと HTML も出力(emcc hello.c -o hello.html) |
| rustc+wasm-bindgen | Rust | JS と文字列・オブジェクトをやり取りするコードを自動で作る |
| Go・Kotlin・Dart など | 各言語 | 各言語のコンパイラが Wasm を出力。GC を使う言語は Wasm 3.0 の GC を使う |
Emscripten の使い方は web.dev と Emscripten の公式ドキュメント、wasm-bindgen は公式ドキュメント(rustwasm の組織の終了に伴い、wasm-bindgen.github.io に移っています)で確かめました。
JavaScriptとWasmの速度比較(実測)
この節では、同じ計算を JavaScript と WebAssembly で書いたとき、どれくらい速さが違うのかが、Node.js と Chromium の実測で分かります。
重い計算は Wasm が約1.6〜2.2倍速く、浮動小数点の計算はほぼ同じ、小さな関数を JS から何度も呼ぶと Wasm が約9〜15倍遅い、という結果でした。 C の同じ関数(clang -O2 の calc.wasm)と、同じ手順の JS を、それぞれ1回空回ししてから7回測り、中央値を取りました。結果の値は JS と Wasm で一致しています。
| 処理(ミリ秒) | JS(Node) | Wasm(Node) | JS(Chromium) | Wasm(Chromium) |
|---|---|---|---|---|
| fib(32) 再帰 | 29.2 | 15.4 | 66.4 | 29.8 |
| 素数の数(1千万まで・ふるい) | 85.5 | 42.1 | 143.6 | 90.9 |
| マンデルブロ集合 800×600(f64) | 70.8 | 71.8 | 101.9 | 110.9 |
| 小さな関数 add を1千万回呼ぶ | 7.5 | 69.0 | 8.6 | 132.0 |
計測した環境は2コアの共有環境で、同じ処理でも回によって最大2倍ほど揺れました。Node は3回の計測の中央値、Chromium は1回の計測(各7回の中央値)です。倍率の傾向は毎回同じでしたが、絶対値は目安です。計測のコード(一部)です。
function measure(fn, runs = 7) {
let r; fn(); const ts = []; // 1回空回しして JIT を温める
for (let i = 0; i < runs; i++) { const t = performance.now(); r = fn(); ts.push(performance.now() - t); }
return { result: r, ms: median(ts) };
}
measure(() => fibJS(32)); // JS
measure(() => instance.exports.fib(32)); // Wasm
measure(() => loopCalls(instance.exports.add, 10000000)); // JS → Wasm を1千万回
ネイティブと wasmtime との比較
同じ C を、ネイティブ(clang -O2)と、wasmtime(Cranelift でコンパイルする実行環境。Python 版)でも動かしました。
| 処理(ミリ秒) | ネイティブ | wasmtime | Wasm(Node) |
|---|---|---|---|
| fib(32) | 10.0 | 8.1 | 15.4 |
| 素数の数(1千万まで) | 48.6 | 51.6 | 42.1 |
| マンデルブロ 800×600 | 80.5 | 83.9 | 71.8 |
揺れの範囲の中で、Wasm はネイティブと同じくらいの速さで動きました。MDN の「ネイティブに近いパフォーマンス」という説明どおりの結果です。
- Wasm が速かったのは、整数の計算と大きな配列の読み書き(fib・ふるい)でした。
- 浮動小数点の単純なループは、JS の JIT も十分に最適化しており、差が出ませんでした。
- JS と Wasm の境目をまたぐ呼び出しには時間がかかります。小さな関数を何度も呼ぶのではなく、配列をまとめて Wasm に渡し、ループも Wasm の中で回すと速さを活かせます。
- 読み込みの時間は、Node で calc.wasm の
instantiateが 0.66〜2.61 ミリ秒、Chromium で fetch を含めて 8.5 ミリ秒でした。
線形メモリと文字列(数値しか渡せない理由)
この節では、Wasm の関数に文字列や配列を渡す方法と、メモリを増やしたときの落とし穴が分かります。
Wasm のメモリは、1本の大きなバイト列(線形メモリ)です。JS からは ArrayBuffer として見え、両側から同じバイトを読み書きできます。 関数には数値しか渡せないので、文字列は「メモリに書き込んで、位置と長さを数値で渡す」方法を取ります。
/* str.c:線形メモリに置かれた UTF-8 のバイト列から、ASCII の母音を数える */
static unsigned char buf[1024];
unsigned char *buffer(void) { return buf; }
int count_vowels(int len) {
int c = 0;
for (int i = 0; i < len; i++) {
unsigned char ch = buf[i] | 0x20;
c += ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u';
}
return c;
}
const { memory, buffer, count_vowels } = instance.exports;
const bytes = new TextEncoder().encode('WebAssembly は JavaScript と一緒に動きます');
const ptr = buffer(); // wasm 側の置き場所(アドレス)
new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes); // 線形メモリへ書き込む
console.log('ptr =', ptr, 'bytes =', bytes.length, 'vowels =', count_vowels(bytes.length));
const view = new Uint8Array(memory.buffer);
console.log('grow 前 view.length =', view.length);
memory.grow(1);
console.log('grow 後 view.length =', view.length, '/ 作り直すと', new Uint8Array(memory.buffer).length);
console.log('count_vowels("Hello") 文字列を直接渡すと =', count_vowels('Hello'));
ptr = 1024 bytes = 51 vowels = 6
grow 前 view.length = 131072
grow 後 view.length = 0 / 作り直すと 196608
count_vowels("Hello") 文字列を直接渡すと = 0
- 1文字列は TextEncoder で UTF-8 のバイト列にする日本語を含む33文字が51バイトになった。長さは文字数ではなくバイト数で渡す
- 2文字列をそのまま渡しても、エラーにならず間違う"Hello" は数値に変換できず0になり、結果は0だった
- 3memory.grow の後はビューを作り直す前に作った Uint8Array は長さ0(切り離された状態)になった。毎回 memory.buffer から作る
- 4メモリは64KiBのページ単位で増える131072(2ページ)→ 196608(3ページ)。減らす命令は無い
このやり取りを毎回手で書くのは手間なので、Rust の wasm-bindgen や Emscripten が、文字列やオブジェクトを受け渡すつなぎのコードを自動で作ります。Wasm 3.0 では JS の文字列を直接扱う「JS String Builtins」も標準に入りました。
WASIとは(ブラウザの外でwasmを動かす)
この節では、WASI が何のためのもので、Node.js と wasmtime で .wasm にファイルを読ませると何が起きるのかが分かります。
WASI(WebAssembly System Interface)は、.wasm がファイル・標準出力・時刻・乱数などを使うための、OS に依存しない決まりです。 公式サイト(wasi.dev)によると、版は 0.1(P1)・0.2(P2)・0.3(P3)があり、2026年10月29日時点で 0.1 は「Legacy」、0.2 は2024年1月末に出て「Stable, superseded by 0.3」、0.3 が「Stable, current」です。0.3 では Component Model に非同期の仕組みが加わりました。対応する実行環境として、Wasmtime・WAMR・WasmEdge・wazero・Wasmer などが挙がっています。
C のプログラムを WASI 向けにコンパイルしました。ファイルを1行読んで表示するだけのものです。
#include <stdio.h>
int main(int argc, char **argv) {
printf("Hello, WASI! argc=%d\n", argc);
const char *path = argc > 1 ? argv[1] : "/data/in.txt";
FILE *f = fopen(path, "r");
if (!f) { perror(path); return 1; }
char line[128];
if (fgets(line, sizeof line, f)) printf("read: %s", line);
fclose(f);
return 0;
}
$ clang --target=wasm32-wasi -O2 -Wl,--strip-all -o hello-s.wasm hello.c
$ wasm-objdump -x -j Import hello.wasm
Import[12]:
- func[0] sig=3 <__imported_wasi_snapshot_preview1_args_get> <- wasi_snapshot_preview1.args_get
- func[9] sig=4 <__imported_wasi_snapshot_preview1_fd_write> <- wasi_snapshot_preview1.fd_write
- func[10] sig=6 <__imported_wasi_snapshot_preview1_path_open> <- wasi_snapshot_preview1.path_open
...
printf や fopen は、wasi_snapshot_preview1 の fd_write・path_open などを import する形になっていました。大きさは、デバッグ情報つきで232,274バイト、--strip-all で32,689バイトです。
Node.js の node:wasi で動かす
import { readFile } from 'node:fs/promises';
import { WASI } from 'node:wasi';
const wasi = new WASI({ version: 'preview1', args: ['hello'], env: {},
preopens: { '/data': './sandbox' } }); // 見せるフォルダーだけ渡す
const mod = await WebAssembly.compile(await readFile('hello-s.wasm'));
const inst = await WebAssembly.instantiate(mod, wasi.getImportObject());
console.log('exit code =', wasi.start(inst));
| 渡した条件 | Node.js 22(node:wasi) | wasmtime-py 50.0.1 |
|---|---|---|
| /data にフォルダーを渡す | read: hello from host file(終了コード0) | 同じ(終了コード0) |
| フォルダーを渡さない | /data/in.txt: No such file or directory | /data/in.txt: No such file or directory |
| /etc/hostname を読む | /etc/hostname: No such file or directory | (渡していない場所は見えない) |
| /data/../ で外へ出る | Capabilities insufficient | Operation not permitted |
.wasm は、実行する側が渡したフォルダーしか見えず、../ で外に出ることもできませんでした。 この「渡された権限だけを使える」考え方を、ケーパビリティ(capability)ベースと呼びます。Node.js では ExperimentalWarning: WASI is an experimental feature and might change at any time が表示されました。Node.js の公式ドキュメントは node:wasi を「Stability: 1 - Experimental」とし、一部の WASI 実行環境が持つ包括的なファイルシステムの安全性は備えていないので、信頼できないコードの実行に頼らないよう書いています。
wasmtime の CLI(公式)
wasmtime の CLI は、公式の手順では Linux・macOS で curl https://wasmtime.dev/install.sh -sSf | bash、Windows では MSI のインストーラーで入れます。実行は wasmtime --dir . foo.wasm のように、wasmtime の引数を .wasm の前に書きます(後ろに書くと .wasm 側の引数になる、と公式に書かれています)。関数を直接呼ぶ wasmtime run --invoke add add.wasm 1 2 もあります。手元では CLI ではなく、同じエンジンを使う Python 版で確かめました。
.wasm ファイルを Linux で直接実行すると cannot execute binary file: Exec format error になりました。.wasm は OS の実行ファイルではなく、必ず実行環境を通して動かします。
WebAssemblyの版と対応ブラウザ(Wasm 3.0)
この節では、WebAssembly の歴史と、2026年10月29日時点の版と主な機能の対応状況が分かります。
| 年 | 出来事 |
|---|---|
| 2015年 | WebAssembly が発表される(それまでは asm.js という JS のサブセットで高速化していた) |
| 2017年3月 | 最初の版(MVP)。2017年中に主要なブラウザが対応 |
| 2019年12月5日 | W3C 勧告になり、正式な Web 標準に |
| 2024年1月末 | WASI 0.2 が出る(wasi.dev) |
| 2025年9月17日 | Wasm 3.0 が標準に。64ビットのメモリ、複数のメモリ、GC、例外処理、末尾呼び出しなど |
WebAssembly 公式の機能表(features.json)で、主な機能が既定で使えるようになった版を確かめました。
| 機能 | Chrome | Firefox | Safari | Node.js |
|---|---|---|---|---|
| SIMD(128ビット) | 91 | 89 | 16.4 | 16.4 |
| スレッド(共有メモリ) | 74 | 79 | 14.1 | 16.4 |
| 末尾呼び出し | 112 | 121 | 18.2 | 20.0 |
| GC | 119 | 120 | 18.2 | 22.0 |
| 例外処理(exnref) | 137 | 131 | 18.4 | 24.15 |
| 64ビットのメモリ | 133 | 134 | Technology Preview のみ | 24.0 |
WebAssembly 1.0 の範囲(最初の版)は、今の主要なブラウザならどれでも動きます。新しい機能を使うときは、この表と、実行時に機能を調べる wasm-feature-detect(公式サイトが案内するライブラリ)で確かめます。スレッドを使うには SharedArrayBuffer が要り、ページを COOP・COEP のヘッダーで分離する必要があります。
WebAssemblyのよくある誤解とエラー(原文つき)
この節では、WebAssembly についてよくある誤解と、手元で出たエラーの原文と直し方が分かります。
よくある誤解
- 「Wasm にすれば何でも速くなる」:手元では浮動小数点のループはほぼ同じで、小さな関数を何度も呼ぶと9〜15倍遅くなりました。測ってから決めます。
- 「JavaScript はいらなくなる」:ブラウザでは DOM・イベント・通信は JS の担当です。Wasm は JS から読み込まれ、JS の関数を import して画面を変えます。
- 「バイナリだから中身を隠せる」:wasm2wat で関数の構造まで読めました。秘密の鍵やパスワードを .wasm に入れて隠すことはできません。
- 「サンドボックスだから安全」:外へは出られませんが、Wasm の中のメモリは守られません。C のバッファあふれは、Wasm の線形メモリの中で起きます。
- 「Wasm は Web 専用」:WASI でサーバー・エッジ・コマンドラインでも動きます。
エラーの原文と直し方
| エラーの原文 | 原因 | 直し方 |
|---|---|---|
| CompileError: WebAssembly.compile(): expected magic word 00 61 73 6d, found 28 6d 6f 64 @+0 | .wat(テキスト)をそのまま渡した。28 6d 6f 64 は「(mod」 | wat2wasm で .wasm にする |
| CompileError: … found 68 65 6c 6c @+0 | .wasm ではないファイル(404の HTML など)を読んだ | URL とサーバーの応答を確かめる |
| TypeError: WebAssembly.instantiate(): Import #0 module="env": module is not an object or function | import に必要な env を渡していない | wasm-objdump -x -j Import で必要な import を見る |
| LinkError: WebAssembly.instantiate(): Import #0 module="env" function="setText": function import requires a callable | 関数の import に関数以外を渡した | 名前と型を合わせて関数を渡す |
| RuntimeError: divide by zero | 整数の0除算(i32.div_s) | 呼ぶ前に0を確かめる |
| RuntimeError: memory access out of bounds | 線形メモリの範囲外(65536番地)を読んだ | 位置と長さを確かめる。必要なら memory.grow |
| RuntimeError: unreachable | unreachable 命令に来た(Rust の panic・C の abort もこれになる) | 元の言語のエラーを、つなぎのコードやログで確かめる |
| RangeError: Maximum call stack size exceeded | 終わらない再帰 | 再帰の終わりの条件を直す |
| TypeError: Cannot convert 1 to a BigInt | i64 の引数に Number を渡した | 1n のように BigInt で渡す |
| bad.wat:5:5: error: unexpected token i32.addd, expected ). | WAT の命令名の打ち間違い | 行と列の位置を見て直す |
| error: type mismatch in implicit return, expected [i32] but got [i64] | 戻り値の型と、最後に積んだ値の型が違う | result の型か命令(i32.const 等)を合わせる |
WebAssembly.validate(bytes) は、テキストのファイルに対して false を返しました。読み込む前に、正しい .wasm かを確かめるのに使えます。
よくある質問
Q. WebAssembly とは何ですか? A. ブラウザや Node.js などで動く、コンパクトなバイナリ形式の低レベルな言語です。C・C++・Rust などで書いたコードのコンパイル先になり、JavaScript から呼び出して使います。2019年に W3C の標準になりました。
Q. wasm とは何ですか? A. WebAssembly の公式の略称です。ファイルの拡張子(.wasm)としても使われます。読み方は「ワズム」が一般的です。
Q. WebAssembly を使うと JavaScript より何倍速くなりますか? A. 処理によります。手元では、再帰と大きな配列の計算が約1.6〜2.2倍速く、浮動小数点の単純なループはほぼ同じ、小さな関数を JS から1千万回呼ぶ処理は約9〜15倍遅くなりました。
Q. WebAssembly を手で書く必要はありますか? A. ほとんどありません。C・C++・Rust・Go などのコンパイラが .wasm を出力します。テキスト形式(WAT)は、仕組みの学習やデバッグで読むときに役立ちます。
Q. WebAssembly から DOM を操作できますか? A. 直接はできません。JS の関数を import して、その関数の中で DOM を書き換えます。Rust の wasm-bindgen や Emscripten は、このつなぎのコードを自動で作ります。
Q. どのブラウザで動きますか? A. 最初の版(1.0)の範囲は、Chrome・Edge・Firefox・Safari の今の版で動きます。GC・例外処理・64ビットのメモリなどの新しい機能は、ブラウザと版によって対応が違うため、WebAssembly 公式の機能表で確かめます。
Q. WASI とは何ですか? A. .wasm がファイル・標準出力・時刻などを使うための、OS に依存しない決まりです。ブラウザの外(wasmtime・Node.js など)で動かすときに使います。渡したフォルダーしか見えない仕組みになっています。
Q. WebAssembly と Docker(コンテナ)の違いは? A. コンテナは OS のユーザー空間ごと固めて動かし、Wasm は関数とメモリだけの小さなモジュールを実行環境の中で動かします。Wasm は起動が速く小さい一方、使える OS の機能は WASI で決められた範囲に限られます。
Q. .wasm ファイルを開くと何が見えますか?
A. 先頭は必ず 00 61 73 6d(\0asm)で、その後に版と、Type・Function・Export・Code などのセクションが続きます。wasm2wat でテキスト形式に戻して読めます。
Q. ブラウザで「Incorrect response MIME type」と出ます。
A. サーバーが .wasm を application/wasm 以外で返しています。サーバーの MIME タイプの設定を直すか、fetch → arrayBuffer() → WebAssembly.instantiate() の順で読み込みます。
Q. asm.js とは何が違いますか? A. asm.js は JavaScript の一部の書き方に限定して高速化する方式で、WebAssembly の前身にあたります。WebAssembly はバイナリ形式で、読み込みと変換がより速く、今は WebAssembly が使われています。
まとめ
- WebAssembly(Wasm)は、ブラウザや Node.js で動くバイナリ形式の命令の集まり。C・C++・Rust などのコンパイル先で、JavaScript から呼び出して使う。
- .wasm の先頭は
00 61 73 6d。足し算1つの .wasm は41バイトで、Type・Function・Export・Code のセクションでできていた。 - 手元の実測では、再帰と配列の計算は JS の約1.6〜2.2倍速く、浮動小数点のループはほぼ同じ、JS から小さな関数を何度も呼ぶと約9〜15倍遅かった。処理はまとめて Wasm に渡す。
- 関数に渡せるのは数値だけ。文字列は線形メモリに書き込んで位置と長さを渡す。memory.grow の後はビューを作り直す。
- WASI を使うとブラウザの外でも動き、渡したフォルダーしか見えない。ブラウザでは MIME タイプ application/wasm と、import の渡し忘れに注意する。
参考・出典
確認日はいずれも 2026年10月29日です。
- MDN「WebAssembly」:https://developer.mozilla.org/ja/docs/WebAssembly
- MDN「WebAssembly.instantiateStreaming()」:https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/instantiateStreaming_static
- web.dev「WebAssembly とは何ですか。また、どこから提供されていますか?」:https://web.dev/articles/what-is-webassembly?hl=ja
- web.dev「Photoshop's journey to the web」:https://web.dev/articles/ps-on-the-web
- WebAssembly 公式サイトのお知らせ(Wasm 3.0、2025年9月17日):https://webassembly.org/news/2025-09-17-wasm-3.0
- WebAssembly 公式サイトの機能の対応表:https://webassembly.org/features/
- WASI「WASI Releases」:https://wasi.dev/releases
- Node.js「WebAssembly System Interface (WASI)」:https://nodejs.org/docs/latest-v22.x/api/wasi.html
- Wasmtime「Installing wasmtime」:https://docs.wasmtime.dev/cli-install.html
- Wasmtime「CLI Options」:https://docs.wasmtime.dev/cli-options.html
- Figma「WebAssembly cut Figma's load time by 3x」:https://www.figma.com/blog/webassembly-cut-figmas-load-time-by-3x/
- Cloudflare「WebAssembly (Wasm)」(Workers):https://developers.cloudflare.com/workers/runtime-apis/webassembly/
- wasm-bindgen ドキュメント:https://wasm-bindgen.github.io/wasm-bindgen/
- WebAssembly Binary Toolkit(wabt):https://github.com/WebAssembly/wabt
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「WebAssembly(Wasm)とは|仕組みとJSとの速度を実測」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。