Chrome と DevTools を使用して、メモリリーク、メモリの肥大化、頻繁なガベージ コレクションなど、ページのパフォーマンスに影響するメモリの問題を見つける方法について説明します。
概要
- Chrome タスク マネージャーを使用して、ページで使用されているメモリ量を調べます。
- Timeline 記録を使用して、メモリの使用量を時系列に表示します。
- ヒープ スナップショットを使用して、デタッチされた DOM ツリー(メモリリークの一般的な原因)を特定します。
- Allocation Timeline 記録を使用して、新しいメモリが JS ヒープに割り当てられるタイミングを調べます。
- JavaScript 参照によって保持されているデタッチされた要素を特定します。
概要
RAIL パフォーマンス モデルの観点から、パフォーマンスに取り組む際はユーザーを第一に考えます。
メモリの問題はユーザーが気付くことが多いため、重要な問題です。ユーザーは次のようなことからメモリの問題に気付く可能性があります。
- 時間が経つにつれ、ページのパフォーマンスが徐々に低下する。これはメモリリークの兆候と考えられます。メモリリークとは、ページ内のバグが原因で、時間が経つにつれページで使用されるメモリ量が徐々に増えていく現象です。
- ページのパフォーマンスが一貫して低い。これはメモリ肥大化の兆候と考えられます。メモリ肥大化とは、ページで使用されるメモリ量が最適なページ速度を保つために必要なメモリ量を超えている状態です。
- 頻繁に、ページのパフォーマンスが低下するか、一時停止しているように見える。これはガベージ コレクションが頻繁に行われている兆候と考えられます。ガベージ コレクションは、ブラウザによってメモリが再利用されるタイミングで行われます。このタイミングはブラウザに左右されます。ガベージ コレクションの実行中は、すべてのスクリプトの実行が一時停止します。そのため、ブラウザによってガベージ コレクションが行われる頻度が増すと、スクリプトの実行が何度も一時停止することになります。
メモリ肥大化: 「使用量が多すぎる」と判断する基準
メモリリークを判断するのは簡単です。サイトのメモリ使用量が徐々に増えていれば、リークが発生しています。ただし、メモリ肥大化の判断はやや困難です。「メモリ使用量が多すぎる」と判断する基準は何でしょう。
これを判断する具体的な数値はありません。理由は端末やブラウザの性能がそれぞれ異なるためです。ハイエンド スマートフォンでスムーズに実行されるページが、ローエンド スマートフォンではクラッシュすることがあります。
重要なのは、RAIL モデルに従ってユーザーを第一に考えることです。ターゲットにするユーザーが通常使用する端末を調べ、その端末でページをテストします。操作性が一貫して低い場合、ページがその端末で利用可能なメモリ容量を超えている可能性があります。
Chrome タスク マネージャによるメモリ使用量のリアルタイム監視
メモリの問題を調べるにあたり、まずは Chrome タスク マネージャを使用します。タスク マネージャは、ページが使用しているメモリ量を表示するリアルタイム モニターです。
Shift+Esc キーを押すか、Chrome のメインメニューに移動して [その他のツール] > [タスク マネージャ] を選択してタスク マネージャを開きます。
タスク マネージャーの表の見出しを右クリックし、[JavaScript memory] を有効にします。
以下の 2 つの列は、ページが使用するメモリについて、それぞれ次の内容を示しています。
- [メモリ使用量] 列は OS メモリを表します。DOM ノードは OS メモリに保存されます。この値が増えている場合は、DOM のノードが作成されています。
[JavaScript Memory] 列は JS ヒープを表します。この列には 2 つの値が含まれます。判断に使用するのは、ライブ数値(かっこ内)です。ライブ数値は、ページ上のアクセス可能なオブジェクトが使用中のメモリ量を表しています。この数値が増えている場合は、新しいオブジェクトが作成されているか、既存のオブジェクトが拡大しています。
パフォーマンス レコーディングでメモリリークを可視化する
調査の出発点として [パフォーマンス] パネルを使用することもできます。[パフォーマンス] パネルには、ページのメモリ使用量が時系列で表示されます。
- DevTools で [パフォーマンス] パネルを開きます。
- [メモリ] チェックボックスをオンにします。
- 録音する。
パフォーマンスのメモリ記録を示すには、次のコードを使用します。
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 detachedTree;
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 を開き、[メモリ] パネルに移動して [ヒープ スナップショット] ラジオボタンをオンにした後、[スナップショットを取得] ボタンをクリックします。
スナップショットの処理と読み込みには時間がかかることがあります。完了したら、左側のパネルでスナップショット([Heap snapshots])を選択します。
[クラス フィルタ] 入力ボックスに「Detached
」と入力して、デタッチされた DOM ツリーを検索します。
カラットを展開してデタッチされたツリーを調べます。
ノードをクリックして詳細を確認します。[Objects] ペインでは、そのノードを参照しているコードの詳細を確認できます。たとえば、次のスクリーンショットでは、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
配列に追加されます。
割り当てタイムラインを記録するには、DevTools を開き、[メモリ] パネルに移動して [タイムライン上の割り当て] ラジオボタンを選択し、
[記録] ボタンを押します。メモリリークの原因と思われる操作を行い、完了したら [録画を停止] ボタンを押します。記録中、以下のスクリーンショットのように、Allocation Timeline に青い縦線が表示される場合は注意が必要です。
このような青い縦線は新しくメモリが割り当てられたことを表します。このような新しいメモリの割り当ては、メモリリークの候補になります。バーを拡大して [Constructor] ペインをフィルタリングすると、指定した期間に割り当てられたオブジェクトのみが表示されます。
オブジェクトを展開して値をクリックすると、[オブジェクト] ペインに詳細が表示されます。たとえば、以下のスクリーンショットのように、新しく割り当てられたオブジェクトの詳細を表示すると、そのオブジェクトが Window
スコープの x
変数に割り当てられていることを確認できます。
関数ごとのメモリ割り当て状況の調査
[メモリ] パネルで [割り当てサンプリング] プロファイル タイプを使用して、JavaScript 関数ごとのメモリ割り当てを表示します。
- [割り当てサンプリング] ラジオボタンを選択します。ページにワーカーがある場合は、[JavaScript VM インスタンスを選択] ウィンドウでプロファイリング ターゲットとして選択できます。
- スタートボタンを押します。
- 調査するページでアクションを実行します。
- すべての操作が完了したら、[停止] ボタンを押します。
DevTools に関数ごとのメモリ割り当ての内訳が表示されます。デフォルトのビューは [Heavy (Bottom Up)] です。このビューには、最もメモリを割り当ての多い関数が一番上に表示されます。
JS 参照によって保持されているオブジェクトを特定する
[デタッチされた要素] プロファイルには、JavaScript コードによって参照されているため保持されているデタッチされた要素が表示されます。
Detached elements プロファイルを記録して、正確な HTML ノードとノード数を表示します。
頻繁なガベージ コレクションの特定
ページが頻繁に一時停止しているように見える場合は、ガベージ コレクションに問題が発生している可能性があります。
Chrome タスク マネージャまたは Timeline メモリ記録を使用して、頻繁に行われるガベージ コレクションを特定できます。タスク マネージャーで、[メモリ] または [JavaScript メモリ] の値が頻繁に増減している場合は、ガベージ コレクションが頻繁に行われていることを表します。Timeline 記録で、JS ヒープまたはノード数のグラフが頻繁に増減する場合は、ガベージ コレクションが頻繁に行われていると考えられます。
問題を特定したら、Allocation Timeline 記録を使用して、メモリを割り当てている箇所と、割り当ての原因となった関数を調べます。