
数ヶ月前、画像処理ライブラリ OpenCV.js を使って Web カメラの映像をリアルタイム処理するプロトタイプを作っていたときのことです。
OpenCV.js は C++ で書かれたコードを WebAssembly(Wasm) にコンパイルして作られており、Wasm ならではのブラウザ上での高速な処理が可能なライブラリです。実際、画像のフィルタ処理や特徴点検出など、ユニットテストの段階では高速に実行でき、開発は一見順調に進んでいるかのように見えました。
ところが、いざアプリケーションに画像処理モジュールを組み込んでみると、起動したカメラが数秒経つとなぜか止まってしまいました。コンソールにもエラーは出ず、Chrome を再起動すればまた数秒だけ動く……そんな不可解な状態に悩まされました。
原因は、Wasm のメモリリーク。
そう、恐ろしいことにC++ 製 Wasm で作られたライブラリを使用する時には JavaScript 側で使い終わったメモリを明示的に解放しなければいけなかったのです。メモリ管理が適切にできていなかったがために Wasm のヒープメモリがみるみるうちに逼迫し、ブラウザのフリーズを引き起こしてしまっていました…
この記事では、JavaScript では見過ごしがちな「Wasm の明示的なメモリ解放」について、その背景と私のようにうっかりメモリリークさせないための対策を主にフロントエンド開発者向けに解説します。
カミナシ StatHack カンパニー(AI プロダクトの検証・開発チーム)の井上です。
カミナシでは主に画像処理系の AI アルゴリズムや Web アプリケーションの開発を担当しています。最近は iPad のカメラで読み取った食品表示ラベルを OCR や画像マッチング技術を駆使して検査するプロダクトの開発に携わっています。
C++ で作成した Wasm を JavaScript や TypeScript から利用する際には以下のコードのように作成したオブジェクトを明示的に delete する必要があります。これを怠ると、ヒープメモリがどんどん蓄積し、最終的にメモリリークが発生します。
JavaScript 側のコード
asyncfunctionmain(){// Wasm module の読み込みconst Module=awaitMyWasmModule();// Wasm 側で用意したクラスのインスタンスを作成const obj=newModule.MyObject();const result= obj.doSomething();// delete を忘れるとメモリリーク obj.delete();// 戻り値も削除する必要あり result.delete();}
C++ 側のコード
#include<emscripten/bind.h>class MyObject {public:MyObject() { } ~MyObject() { } ResultdoSomething() {returnnewResult(); }};// C++ で記述した関数やクラスを JavaScript から呼び出せるようにするための設定EMSCRIPTEN_BINDINGS(my_module) { emscripten::class_<MyObject>("MyObject") .constructor<>() .function("doSomething", &MyObject::doSomething);}
Wasm では、JavaScript のガベージコレクタによるメモリ管理と独立して、メモリを確保・管理します。簡単のために、 この記事では Wasm が確保するメモリ領域のことを“Wasm メモリ” と呼ぶことにします。
前述の仕様により、たとえ JavaScript 側からコンストラクタを呼び出していたとしても Wasm 側で用意したオブジェクトの実体は Wasm メモリ上にあるため、JavaScript のガベージコレクションによる自動メモリ解放の対象外となります。
したがって、Wasm 側で作成したオブジェクトは、ビルドに利用した言語(本記事では C++)の仕様に従ってメモリを解放する必要があります。
C++ で作成した Wasm の場合は、
const obj=newModule.MyObject();
のように JavaScript 側から Wasm オブジェクトのコンストラクタを呼び出すと、C++ 側の
MyObject* obj =newMyObject();
に相当する処理が実行されて Wasm メモリ上にインスタンスが確保されます。
そのため、この変数のメモリを解放するためには後始末として C++ での
delete obj;に対応する処理を実行する必要があり、これを JavaScript 側から行うのが
obj.delete();
です。
以上をまとめると Wasm で作成したオブジェクトについては
という構造になっています。

Wasm のオブジェクトを作成してからスコープを抜けるまでに処理が多く挟まると、delete による明示的な解放処理を書き忘れたり、delete の前に例外が起こってしまいメモリを解放できなかったりする可能性があります。そのため、 Emscripten(C++ → Wasm のコンパイラ) の公式では以下のような try…finally を利用した書き方を推奨しています。
asyncfunctionmain(){const Module=awaitMyWasmModule();const obj=newModule.MyObject();try{ obj.method();}finally{ obj.delete();}}
この方法はシンプルではあるものの、delete するべきリソースが多い場合などでは try…finally のネストが深くなりやすいなどの課題があります。
解放忘れを防ぐ工夫があるとはいえ、Wasm で確保されるメモリを常に意識してdelete を忘れずに書かないといけないというのは、JavaScript のようなガベージコレクションのある言語に慣れた開発者からすればストレスに感じることがあるかもしれません。
Wasm の実装や JavaScript からの呼び出しで工夫をすることで明示的な delete の記述を場合によっては回避することができるので、ここでは、その方法をいくつかご紹介します。
Emscripten には値オブジェクト (Value Object) という仕組みがあります。
データ構造を Wasm から JavaScript に返す際に、C++ で class ではなく struct として実装し、値オブジェクトとして以下のように設定することができます。
C++
struct Person {.std::string name;int age; };// 構造体 Person を値オブジェクトとして JavaScript から呼び出せるように設定EMSCRIPTEN_BINDINGS(my_module) { value_object<Person>("Person") .field("name", &Person::name) .field("age", &Person::age);}
JavaScript
const person=newModule.Person();person.name="Taro";person.age=20;
値オブジェクトとして登録した場合、Wasm はデータを JavaScript オブジェクトにコピーして渡すようになるため、JavaScript のガベージコレクションの対象となり明示的な delete が不要になります。ただし、値オブジェクトの名の通り、メソッドを持たない構造体でないと値オブジェクトにはできないので注意が必要です。
Emscripten にはC++ のスマートポインタを使ったクラス管理の機能があります。
スマートポインタは端的に説明すると「所有者がいなくなったら自動的にメモリを解放する仕組み」です。
C++
#include<emscripten/bind.h>class MyObject {public:MyObject() { } ~MyObject() { } ResultdoSomething() {returnnewResult(); }};EMSCRIPTEN_BINDINGS(my_module) { emscripten::class_<MyObject>("MyObject") .smart_ptr_constructor("MyObject", &std::make_shared<MyObject>);}
このようにsmart_ptr_constructor を指定することで、内部的に参照カウントが管理されます。
例えば、JavaScript 側で
const obj =new Module.MyObject();
のような形でオブジェクトを作成した時、変数 obj が参照を持っているためカウントが1になりますが、この変数のスコープを抜けるとカウントが0になり、自動でメモリが解放されるため、JavaScript のガベージコレクションと連動するような形で Wasm メモリが管理されることになります。
ただし、この機能はFinalizationRegistryという JavaScript の機能を利用して実装されているため注意が必要です。ファイナライザの処理はガベージコレクションがトリガーになっています。しかし、ガベージコレクションは実行タイミングが非決定的であり、確実に実行される保証もないため、これを過信したリソース管理は危険です。
Emscripten のドキュメントでも「いかなるC++ のオブジェクトも JavaScript コード側で明示的に delete すべき」と強く推奨しており、ファイナライザ経由の自動解放はあくまで最終的にメモリリークを避ける保険的措置と位置付けられています。
It is strongly recommended that JavaScript code explicitly deletes any C++ object handles it has received.https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#automatic-memory-management
また、FinalizationRegistry は2025年7月現在では広範なブラウザでサポートされているものの古い環境では利用できない可能性もあり、この場合スマートポインタを登録していても自動解放されずリークしてしまいます。したがって、スマートポインタを使った場合でも、やはり明示的にdeleteを呼ぶのが安全です。
JavaScript の新しい言語機能として、using構文が一部の環境で使えるようになっています。Emscripten はこの仕組みにも対応しており、以下のような書き方が可能です。
using obj=newModule.MyObject();obj.method();// 処理がスコープから外れると自動で obj.delete() が呼ばれる
usingで宣言したオブジェクトはスコープ終了時に自動的に解放されるため、明示的にdeleteを呼ぶ必要がありません。
なお、2025年7月現在、using構文は Safari など一部の環境では未実装であるため、利用できる環境かどうか注意が必要です。
Wasm は JavaScript / TypeScript だけでは達成できない高性能な処理を実現できるため魅力的ですが、ここまで見てきたようにメモリ管理の考え方が求められる場面もあります。
delete の呼び忘れによるメモリリークは、開発中は気づきにくく、運用後にブラウザのクラッシュや性能低下の原因となることもあるため、Wasm のメモリ管理の仕様をよく理解する必要があります。
また、単に Wasm とひとくくりにすることはできず、ビルドした言語によってメモリ管理の仕方が異なることにも注意が必要です。例えば、Rust(wasm-bindgen)製の Wasm では通常、Rust の所有権のルールに従うため、JavaScript 側でのメモリの明示的な解放は必要ないとされています。しかし、JavaScript 側での所有権の管理の実現に FinalizationRegistry を利用しているようなので、前述の C++ 製 Wasm のスマートポインタ実装と同様に明示的にメモリを解放しない限りはメモリの解放タイミングが予測できないなどの注意事項があることに気をつけなければなりません。
我々のチームが Wasm を使うに至った経緯や Wasm を Web アプリケーションに組み込む上での開発体制などについて、同じチームの渡邉が詳しく書いているのでぜひご覧くださいkaminashi-developer.hatenablog.jp
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。