なぜかWebページが重くなる?DevTools Memoryタブで探るメモリリークの探し方
Webサイトを開発していると、「なぜかページが徐々に重くなる」「長時間開いているとブラウザのメモリ使用量が異常に増える」といった現象に遭遇することがあります。その原因の一つに、メモリリークが挙げられます。
メモリリークは、不要になったメモリ領域が解放されずに残り続けてしまう状態を指します。特にJavaScriptのようなガベージコレクション(GC)を持つ言語では、本来GCが不要になったオブジェクトのメモリを自動的に解放してくれるはずですが、意図しない参照が残ってしまうことでリークが発生します。
この記事では、Web開発のジュニア開発者の皆様が直面しやすい、クライアントサイド(ブラウザ)でのメモリリークに焦点を当て、ブラウザの開発者ツール(DevTools)に含まれるMemoryタブを使って、どのようにメモリリークを発見し、その原因を特定するのかをステップバイステップで解説します。
メモリリークが引き起こす問題
メモリリークが発生すると、以下のような問題がユーザーやアプリケーションにもたらされる可能性があります。
- パフォーマンスの低下: 使用可能なメモリが減少し、GCの処理時間が増加するため、ページの表示や操作が遅くなります。
- ブラウザやOSの不安定化: メモリ使用量が閾値を超えると、ブラウザやシステム全体が不安定になったり、クラッシュしたりする可能性があります。
- ユーザーエクスペリエンスの悪化: ページの応答性が低下し、快適に操作できなくなります。
ブラウザDevToolsのMemoryタブとは
主要なモダンブラウザ(Chrome, Firefox, Edgeなど)の開発者ツールには、Memoryタブが搭載されています。このタブでは、Webページが使用しているメモリの状況を詳細に分析することができます。
Memoryタブで主に利用される機能には、以下のようなものがあります。
- Heap snapshot (ヒープスナップショット): 特定の時点でのJavaScriptヒープメモリ(オブジェクト、DOMノードなど)の状態を記録します。どのオブジェクトがどのくらいのメモリを使用しているか、どのような参照関係にあるかなどを詳細に調べることができます。
- Allocation instrumentation on timeline (アロケーションタイムライン): 一定期間におけるメモリ割り当てとGCイベントを記録し、メモリがどのように増減しているかを時系列で確認できます。
メモリリークの診断では、主にHeap snapshot機能が強力な武器となります。
Memoryタブを使ったメモリリーク診断の基本手順
メモリリークの診断は、以下の基本的なステップで進めるのが一般的です。
- 疑わしい操作の特定: メモリリークが疑われる状況(例: 特定のページの表示、モーダルウィンドウの開閉、Ajaxリクエストの繰り返しなど)を特定します。
- ベースラインのスナップショット取得: 何も操作を行っていない、クリーンな状態(または操作開始前)でHeap snapshotを取得します。
- 疑わしい操作の実行: メモリリークが疑われる操作を数回(2~3回程度)繰り返します。操作ごとにページを離れるなど、本来メモリが解放されるべきシナリオを再現します。
- 比較用のスナップショット取得: 操作を繰り返した後、Heap snapshotを再度取得します。
- スナップショットの比較と分析: 取得した複数のスナップショットを比較し、不要なオブジェクトが増加し続けていないかを確認します。
この手順を、Chrome DevToolsのMemoryタブを例に具体的に見ていきましょう。
Step 1: DevToolsを開きMemoryタブを選択
デバッグしたいページを開き、F12キーまたは右クリックメニューから「検証」(または「開発者ツール」)を選択してDevToolsを開きます。「Memory」タブをクリックして選択します。もしMemoryタブが表示されていない場合は、タブの右側にある「その他のツール」アイコン(三点リーダーなど)から選択してください。
Step 2: ベースラインのHeap snapshotを取得
Memoryタブを開いたら、プロファイルタイプの選択肢から「Heap snapshot」が選択されていることを確認します。
(注:上記は表示イメージです。実際のUIとは異なる場合があります。)
準備ができたら、左上の丸い記録ボタン、または画面下部にある「Take snapshot」ボタンをクリックします。
(注:上記は表示イメージです。実際のUIとは異なる場合があります。)
スナップショットの取得には少し時間がかかる場合があります。完了すると、左側のパネルにスナップショットが一覧表示されます(例: Snapshot 1
)。これがベースラインとなります。
Step 3: 疑わしい操作を複数回実行
メモリリークが疑われる操作を数回実行します。例えば、特定のデータを表示するモーダルを開いて閉じる、Ajaxリクエストを繰り返し実行するなど、メモリが解放されるはずの操作を意識的に行います。この時、操作の間にGCを強制的に実行させるために、DevToolsのPerformanceタブなどでゴミ箱アイコンをクリックする方法もありますが、まずは自然な操作で確認を進めます。
Step 4: 比較用のHeap snapshotを取得
操作を数回繰り返した後、再度「Take snapshot」ボタンをクリックして、新しいスナップショットを取得します(例: Snapshot 2
, Snapshot 3
)。これにより、操作によってメモリ使用量がどのように変化したかを比較するためのデータが揃います。
Step 5: スナップショットの比較と分析
取得した複数のスナップショットを比較します。例えば、Snapshot 2
を選択した状態で、サマリービューの上部にある比較対象のドロップダウンメニューからSnapshot 1
を選択します。
(注:上記は表示イメージです。実際のUIとは異なる場合があります。)
比較ビューに切り替わると、各オブジェクトの項目に「#Delta」「Size Delta」などの列が表示されます。「#Delta」はオブジェクトインスタンス数の増減、「Size Delta」はメモリ使用量の増減を示します。
メモリリークの兆候として最も重要なのは、本来インスタンス数が操作後にゼロになるべきオブジェクト(例: 閉じられたモーダルに関連するDOMノードやJavaScriptオブジェクト)や、繰り返し操作によってインスタンスが増加し続けるオブジェクトの「#Delta」や「Size Delta」が正の値になっていることです。
例えば、モーダルを開閉する操作を繰り返した場合、モーダルに関連するDOMノード(HTMLDivElement
など)や、モーダル表示を制御するJavaScriptオブジェクトのインスタンス数が、開閉操作を繰り返すたびに増加し続ける(Snapshot 3
vs Snapshot 2
で正のDeltaが見られる)のであれば、そのオブジェクトがリークしている可能性が非常に高いです。
疑わしいオブジェクトを見つけたら、その項目をクリックして詳細を確認します。詳細ビューでは、そのオブジェクトを「Retainers」リストを見ることができます。Retainersは、そのオブジェクトがメモリ上に保持されている理由、すなわちそのオブジェクトを参照している他のオブジェクトやクロージャなどを示します。このRetainersパスをたどっていくことで、どのオブジェクトからの参照がリークを引き起こしているのか、原因となっているコード箇所を特定する手がかりを得られます。
(注:上記は表示イメージです。実際のUIとは異なる場合があります。)
メモリリークのよくある原因と対策
Heap snapshotの分析で見つかる可能性のある、ジュニア開発者が遭遇しやすいメモリリークの原因と、その対策の例をいくつか挙げます。
- Detached DOMノード: DOMから削除されたにもかかわらず、JavaScriptコードから参照が残っている要素。
- 対策: イベントリスナーを削除したり、DOM要素への参照を持つオブジェクトが適切にガーベージコレクトされるように、不要になった参照を
null
にするなどを検討します。
- 対策: イベントリスナーを削除したり、DOM要素への参照を持つオブジェクトが適切にガーベージコレクトされるように、不要になった参照を
- setInterval/setTimeoutのクリア忘れ:
setInterval
やsetTimeout
で設定したタイマー処理を、不要になったときにclearInterval
やclearTimeout
で停止し忘れている場合。特に、これらのタイマー内で外部のスコープの変数を参照していると、その変数や関連オブジェクトがリークの原因となります。- 対策: タイマー処理が不要になるタイミング(例: コンポーネントのアンマウント時、ページの移動時)で必ずクリア処理を実行します。
- グローバル変数への意図しない参照: 関数内などで
var
なしで変数を宣言してしまい、意図せずグローバルオブジェクト(window
)のプロパティになってしまう場合。- 対策: 厳格モード(
'use strict';
)を使用する、常にconst
またはlet
を使用する、グローバルスコープを汚染しないように注意する。
- 対策: 厳格モード(
- イベントリスナーの削除忘れ: DOM要素などにイベントリスナーを追加した後、要素が削除されたり、リスナーが不要になったりしても、
removeEventListener
で削除し忘れている場合。- 対策: 要素が削除される前や、イベントリスナーが不要になるライフサイクルイベントで、対応する
removeEventListener
を呼び出します。
- 対策: 要素が削除される前や、イベントリスナーが不要になるライフサイクルイベントで、対応する
- クロージャによる意図しない参照: クロージャ内で外側のスコープの大きなオブジェクトを参照している場合、クロージャが生きている間はオブジェクトも解放されません。意図せずクロージャが長く保持されてしまうとリークの原因になります。
- 対策: クロージャ内で参照する外部変数は必要最小限にする、不要になったクロージャへの参照を解除するなど。
まとめ
メモリリークはWebアプリケーションのパフォーマンスに深刻な影響を与える可能性がありますが、ブラウザの開発者ツール、特にMemoryタブのHeap snapshot機能を活用することで、効果的に診断し、原因を特定することが可能です。
この記事で解説したHeap snapshotの取得と比較、Retainersによる参照パスの追跡といった基本的な手順は、メモリリーク解決に向けた強力な手がかりとなります。ぜひ、ご自身の開発プロジェクトで「なぜか重い」と感じる場面があれば、Memoryタブを開いて、メモリの状況を観察してみてください。
原因の特定には、JavaScriptの参照の仕組みやガベージコレクションに関する理解も役立ちます。地道な分析が必要となる場合もありますが、ツールを正しく使いこなすことで、難解なメモリ関連の問題も一つずつ解決していくことができるでしょう。効率的なデバッグスキルを身につけ、より安定した高品質なWebアプリケーション開発を目指しましょう。