Chrome と DevTools を使用して、メモリリーク、メモリの肥大化、頻繁なガベージ コレクションなど、ページのパフォーマンスに影響するメモリの問題を見つける方法について説明します。
TL;DR
- Chrome タスク マネージャを使用して、ページで現在使用されているメモリ量を調べます。
- Timeline 記録を使用して、メモリの使用量を時系列に表示します。
- ヒープ スナップショットを使用して、デタッチされた DOM ツリー(メモリリークの一般的な原因)を特定します。
- Allocation Timeline 記録を使用して、新しいメモリが JS ヒープに割り当てられるタイミングを調べます。
概要
RAIL パフォーマンス モデルの観点から、ユーザーを第一に考えます。
メモリの問題はユーザーが気付くことが多いため、重要な問題です。 ユーザーは次のようなことからメモリの問題に気付く可能性があります。
- 時間が経つにつれ、ページのパフォーマンスが徐々に低下する。これはメモリリークの兆候と考えられます。 メモリリークとは、ページ内のバグが原因で、時間が経つにつれページで使用されるメモリ量が徐々に増えていく現象です。
- ページのパフォーマンスが一貫して低い。これはメモリ肥大化の兆候と考えられます。 メモリ肥大化とは、ページで使用されるメモリ量が最適なページ速度を保つために必要なメモリ量を超えている状態です。
- 頻繁に、ページのパフォーマンスが低下するか、一時停止しているように見える。これはガベージ コレクションが頻繁に行われている兆候と考えられます。 ガベージ コレクションは、ブラウザによってメモリが再利用されるタイミングで行われます。 このタイミングはブラウザに左右されます。ガベージ コレクションの実行中は、すべてのスクリプトの実行が一時停止します。そのため、ブラウザによってガベージ コレクションが行われる頻度が増すと、スクリプトの実行が何度も一時停止することになります。
メモリ肥大化: 「使用量が多すぎる」と判断する基準
メモリリークを判断するのは簡単です。サイトのメモリ使用量が徐々に増えていれば、リークが発生しています。 ですが、メモリ肥大化の判断はやや困難です。 「メモリ使用量が多すぎる」と判断する基準は何でしょう。
これを判断する具体的な数値はありません。理由は端末やブラウザの性能がそれぞれ異なるためです。 ハイエンド スマートフォンでスムーズに実行されるページが、ローエンド スマートフォンではクラッシュすることがあります。
重要なのは、RAIL モデルに従ってユーザーを第一に考えることです。ターゲットにするユーザーが通常使用する端末を調べ、その端末でページをテストします。操作性が一貫して低い場合、ページがその端末で利用可能なメモリ容量を超えている可能性があります。
Chrome タスク マネージャによるメモリ使用量のリアルタイム監視
メモリの問題を調べるにあたり、まずは Chrome タスク マネージャを使用します。 タスク マネージャは、ページが現在使用しているメモリ量を表示するリアルタイム モニターです。
Shift+Esc キーを押すか、Chrome のメインメニューに移動して [More tools] > [Task manager] を選択してタスク マネージャを開きます。
タスク マネージャの表の見出しを右クリックし、[JavaScript memory] を有効にします。
以下の 2 つの列は、ページが使用するメモリについて、それぞれ次の内容を示しています。
- [Memory] 列はネイティブメモリを表します。DOM のノードはネイティブメモリに格納されます。 この値が増えている場合は、DOM のノードが作成されています。
- [JavaScript Memory] 列は JS ヒープを表します。この列には 2 つの値が表示されます。 判断に使用するのは、ライブ数値(かっこ内)です。 ライブ数値は、ページ上のアクセス可能なオブジェクトが使用中のメモリ量を表しています。 この数値が増えている場合は、新しいオブジェクトが作成されているか、既存のオブジェクトが拡大しています。
Timeline 記録によるメモリリークの表示
まずは [Timeline] パネルを使用して調査を始めることもできます。 [Timeline] パネルには、ページのメモリ使用量が時系列で表示されます。
- DevTools で [Timeline] パネルを開きます。
- [Memory] チェックボックスをオンにします。
- 記録を開始します。
ヒント: 記録の開始時と停止時に強制的にガベージ コレクションを行うことをお勧めします。
記録中に ガベージ コレクションの実行 ボタン () をクリックして、強制的にガベージ コレクションを行います。
以下のコードを使用して、Timeline のメモリ記録について説明します。
var x = [];
function grow() {
for (var i = 0; i < 10000; i++) {
document.body.appendChild(document.createElement('div'));
}
x.push(new Array(1000000).join('x'));
}
document.getElementById('grow').addEventListener('click', grow);
コード内で参照しているボタンがクリックされるたびに、1 万個の div
ノードがドキュメント本文に追加され、100 万個の x
文字から成る文字列が x
配列にプッシュされます。このコードを実行すると、以下のスクリーンショットのような Timeline 記録が生成されます。
まず、ユーザー インターフェースを説明します。概要 ペイン([NET] の下)の HEAP グラフは、JS ヒープを表します。概要ペインの下にはカウンターペインがあります。ここでは、JS ヒープ(概要 ペインの HEAP グラフと同じ)、ドキュメント、DOM ノード、リスナー、GPU メモリ別にメモリ使用量が表示されます。チェックボックスをオフにすると、そのメモリ量がグラフに表示されなくなります。
このスクリーンショットと比較してコードを分析します。ノードカウンター(緑色のグラフ)を見ると、コードと明らかに一致しているのがわかります。ノード数が段階的に増加しています。
ノード数は grow()
を呼び出すたびに増加すると考えられます。
JS ヒープのグラフ(青のグラフ)は単純ではありません。ベスト プラクティスを踏まえると、最初の落ち込みは実際にガベージ コレクションが強制的に行われたことを示します(ガベージ コレクションは、ガベージ コレクションの実行ボタンをクリックして行います)。記録が進んでいくと、JS ヒープサイズが急上昇しているのが分かります。これは不自然ではなく、想定できます。JavaScript コードでは、ボタンがクリックされるたびに DOM ノードが作成され、100 万文字から成る文字列を作成するときに多くの処理を行います。重要なのは、開始時よりも終了時の方が JS ヒープが高くなっている点です(「開始時」とはガベージ コレクションを強制的に行った直後を指します)。実際に JS ヒープサイズまたはノードサイズが増加していくパターンを見つけた場合は、メモリリークの可能性を考えます。
ヒープ スナップショットによるデタッチされた DOM ツリーのメモリリークの検出
DOM ノードのガベージ コレクションは、ページの DOM ツリーまたは JavaScript コードからそのノードが参照されなくなった時点で行われます。 ノードが DOM ツリーから削除されても、一部の JavaScript が引き続きそのノードが参照されていることを「デタッチされる」と言います。
デタッチされた DOM ノードは、メモリリークの一般的な原因になります。このセクションでは、DevTools のヒープ プロファイラを使用して、デタッチされたノードを特定する方法について説明します。
以下は、デタッチされた DOM ノードのシンプルな例です。
var detachedNodes;
function create() {
var ul = document.createElement('ul');
for (var i = 0; i < 10; i++) {
var li = document.createElement('li');
ul.appendChild(li);
}
detachedTree = ul;
}
document.getElementById('create').addEventListener('click', create);
コードで参照されているボタンをクリックすると、10 個の子 li
を持つ ul
ノードが作成されます。
これらのノードはコードからは参照されていますが、DOM ツリーには存在しないため、デタッチされた状態になります。
ヒープ スナップショットはデタッチされたノードを特定する 1 つの手段です。名前が示すように、ヒープ スナップショットは、スナップショットの取得時点でページの JS オブジェクトと DOM ノードにメモリがどのように分散されているかを示します。
スナップショットを取得するには、DevTools を開き、[Profiles] パネルに移動して [Take Heap Snapshot] ラジオボタンをオンにした後、[Take Snapshot] ボタンをクリックします。
スナップショットの処理と読み込みには時間がかかることがあります。完了したら、左側のパネルでスナップショット([HEAP SNAPSHOTS])を選択します。
クラス フィルタ テキストボックスに「Detached
」と入力して、デタッチされた DOM ツリーを検索します。
カラットを展開してデタッチされたツリーを調べます。
黄色でハイライト表示されているノードには、JavaScript コードからの直接参照が含まれています。 赤でハイライト表示されたノードには直接参照は含まれていません。それらは黄色のノードのツリーに含まれているという理由で表示されているだけです。 通常は、黄色のノードに注目します。 黄色のノードが必要以上に表示されないようにコードを修正します。また、黄色のノードツリーに含まれている赤のノードも取り除きます。
さらに詳しく調べるには、黄色のノードをクリックします。[Object] ペインでは、そのノードを参照しているコードの詳細を確認できます。
たとえば、以下のスクリーンショットでは、detachedTree
変数がノードを参照しているのがわかります。
この特定のメモリリークを解決するには、detachedTree
を使用するコードを調べ、不要になった時点でノードへの参照を削除します。
Allocation Timeline による JS ヒープのメモリリークの特定
Allocation Timeline も、JS ヒープのメモリリークを追跡できるツールです。
以下のコードを使用して、Allocation Timeline について説明します。
var x = [];
function grow() {
x.push(new Array(1000000).join('x'));
}
document.getElementById('grow').addEventListener('click', grow);
コードで参照されているボタンがクリックされるたびに、100 万文字から成る文字列が x
配列に追加されます。
Allocation Timeline を記録するには、DevTools を開き、[Profiles] パネルに移動して、[Record Allocation Timeline] ラジオボタンをオンにし、[Start] ボタンをクリックします。メモリリークの原因となる疑いのある操作を実行し、完了したら 記録の停止 ボタン () をクリックします。
記録中、以下のスクリーンショットのように、Allocation Timeline に青い縦線が表示される場合は注意が必要です。
このような青い縦線は新しくメモリが割り当てられたことを表します。このような新しいメモリの割り当ては、メモリリークの候補になります。 縦線を選択すると、指定した期間に割り当てられたオブジェクトのみが表示されるように、[Constructor] ペインをフィルタリングすることができます。
オブジェクトを展開して値をクリックすると、詳細が [Object] ペインに表示されます。
たとえば、以下のスクリーンショットのように、新しく割り当てられたオブジェクトの詳細を表示すると、そのオブジェクトが Window
スコープの x
変数に割り当てられていることを確認できます。
関数ごとのメモリ割り当て状況の調査
[Record Allocation Profiler] という種類のプロファイルを使って、JavaScript 関数ごとのメモリ割り当てを表示します。
- [Record Allocation Profiler] ラジオボタンをオンにします。ページにワーカーがある場合、[Start] ボタンの隣にあるドロップダウン メニューを使ってプロファイルの対象として選択できます。
- [Start] ボタンを押します。
- ページで調査したいアクションを実行します。
- アクションがすべて完了したら [Stop] ボタンを押します。
DevTools に関数ごとのメモリ割り当ての内訳が表示されます。既定のビューは [Heavy (Bottom Up)] です。このビューには、最もメモリを割り当ての多い関数が一番上に表示されます。
頻繁なガベージ コレクションの特定
ページが頻繁に一時停止しているように見える場合は、ガベージ コレクションに問題が発生している可能性があります。
Chrome タスク マネージャまたは Timeline メモリ記録を使用して、頻繁に行われるガベージ コレクション特定します。 タスク マネージャで、[メモリ] または [JavaScript メモリ] の値が頻繁に増減している場合は、ガベージ コレクションが頻繁に行われていることを表します。 Timeline 記録で、JS ヒープまたはノード数のグラフが頻繁に増減する場合は、ガベージ コレクションが頻繁に行われていると考えられます。
問題を特定したら、Allocation Timeline 記録を使用して、メモリを割り当てている箇所と、割り当ての原因となった関数を調べます。