はじめに
JavaScriptアプリケーションが動作していると、時間経過とともに動作が重くなったり、不安定になったりすることがあります。
その原因の一つに「メモリリーク」があります。
本記事では、JavaScriptのメモリ管理の基本から、ChromeのDevToolsを用いたメモリリークの検出方法までを紹介します。
メモリリークとは
メモリリークとは、不要になったメモリ領域が何らかの理由で解放されず、保持し続ける現象のことを指します。これにより、利用可能なメモリが徐々に減少し、システムのパフォーマンス低下や、最悪の場合にはクラッシュを引き起こす可能性があります。
JavaScriptエンジンのメモリ管理
JavaScriptエンジンはスタック領域とヒープ領域の二つのメモリ領域を持っており、扱うデータの種類によって2つのメモリ領域を使い分け、メモリ領域の割り当てを行います。
スタック領域
- コンパイル時に必要なメモリ領域のサイズが決まる値をスタック領域に割り当てます
- プリミティブ型、オブジェクトや関数の参照など
ヒープ領域
- 実行時に必要なメモリ領域のサイズが決まる値(動的なデータ)をヒープ領域に割り当てます
- オブジェクト、関数、配列など
ヒープ領域の使われなくなったメモリ領域は、ガベージコレクションによって自動的に解放されるはずですが、参照が残ってしまうとメモリリーク
が発生します。
メモリリークの主な原因
1. グローバル変数
JavaScriptは変数宣言されていない変数を、暗黙的にwindow
(ブラウザの場合)やglobal(Node.js)にバインドされ、アプリケーション終了まで解放されません。
function createLeak() {
leadked = "I'm global"; // let/constを使っていない
}
createLeak();
window.leakedとして残り続ける
2.イベントリスナー
DOMノードにイベントリスナーを登録した後、ノードを削除しても、リスナーで参照が残っているとGCされません。
const button = document.getElementById("myBotton");
function onClick() {
console.log("clicked");
}
button.addEventListener("click", onClick);
// あとでボタンを削除しても参照は残り続ける
button.remove();
3.クロージャ
クロージャは関数にその外側のスコープにアクセスする機能を提供します。
このとき、不要なデータまで残し続けてしまうとメモリリークになります。
function createClosure() {
const largeData = new Array(1_000_000).fill("x");
return function inner() {
console.log("Still holding largeData");
};
}
const hold = createClosure();
// holdが使われ続けている限り largeData はGCされない
4.setInterval / setTimeout の解放忘れ
setTimeout
やsetInterval
などを削除せず、バックグラウンドで動き続けてしまうとメモリリークが発生します。
const timer = setTimeout(() => {
console.log("タイマーが実行されました。");
}, 1000);
// 不要になったらclearInterval / clearTimeout を呼ぶ
clearTimeout(timer);
メモリリークの発見方法
ChromeのDevToolsのPerformanceパネルとMemroryパネルを使って、メモリリークを検知・分析する方法を紹介します。
Performanceパネル
- Devtoolsを開き、「Performance」タブを選択する
- 左上の「Record」ボタンをクリック
- アプリを操作し、計測したい処理を実行
- 操作が終わったら「Stop」ボタンをクリックし記録を終了
- 上部の「Memory」にチェックを入れ、青い線(使用中のJavaScriptヒープ)の挙動を確認
以下の画像のように青いグラフが時間とともに上昇を続け、GCが発生していても一向に下がらない場合、メモリリークが発生していると考えられます。
Memoryパネル
3点ヒープダンプ法を使用することで、効率的にメモリリーク箇所を特定することができます。3点ヒープダンプ法は次の3つのタイミングでスナップショットを取得し、それぞれの差分を分析します。
- アプリ起動後、初期状態で
Heap Snapshot #1
を取得 - メモリリークが疑われる処理を1度だけ実行し、
Heap Snapshot #2
を取得 - 同処理を複数回繰り返し、
Heap Snapshot #3
を取得
メモリリークの特定ポイント
- Heap Snapshot #1 と #2 の間で割り当てられ、
- Heap Snapshot #3 にも残っているオブジェクト
→ これがメモリリークの可能性が高いオブジェクトです。
Snapshot3
を選択した状態で「Objects allocated between Snapshot 1 and Snapshot 2」を選ぶと、 この条件に合致するオブジェクトだけをフィルタして表示できます。
フィルタしたオブジェクト一覧のRetained Size(保持サイズ)の大きいオブジェクトから確認していき、不要な参照や意図しない保持がないかをRetainersタブで辿ります。問題のある参照元を特定したら、コードの見直しやイベントリスナーの解除を行いメモリリークを解消しましょう。
まとめ
- メモリリークとは不要になったメモリ領域がGCによって解放されず、保持し続ける現象のこと
- 主な原因は「グローバル変数」、「イベントリスナー」、「クロージャ」、「setInterval / setTimeout の解放忘れ」
- Performanceタブでメモリリークを検知し、Memoryタブで
3点ヒープダンプ法
を使用し原因を特定することができる