Chrome と DevTools を使用して、メモリ リーク、メモリの肥大化、頻繁なガベージ コレクションなど、ページ パフォーマンスに影響を与えるメモリ問題を検出する方法を紹介します。
Summary #
- Chrome タスク マネージャーを使用して、ページが現在使用しているメモリ量を確認します。
- ヒープ スナップショットを使用して、切り離された DOM ツリー (メモリ リークの一般的な原因) を識別します。
- アロケーション タイムライン レコーディングを使用して、JS ヒープに新しいメモリが割り当てられたときを確認します。
Overview #
RAIL パフォーマンス モデルの精神に則り、パフォーマンスの取り組みの焦点はユーザーであるべきです。
- ページのパフォーマンスは、時間の経過とともに徐々に悪くなります。 これは、メモリ リークの症状である可能性があります。
- ページのパフォーマンスが時間の経過とともに徐々に悪くなる。 これは、メモリの肥大化の症状である可能性があります。
- ページのパフォーマンスが遅延したり、頻繁に停止したりします。 これはおそらく、頻繁なガベージコレクションの症状です。 ガベージコレクションとは、ブラウザがメモリを回収することです。 これがいつ起こるかはブラウザが決定します。 ガベージコレクション中は、すべてのスクリプトの実行が一時停止します。
メモリの肥大化: どのくらいが「多すぎる」のでしょうか?
メモリ リークを定義するのは簡単です。 サイトがより多くのメモリを使用するようになれば、リークが発生していることになります。 しかし、メモリの肥大化は、特定するのが少し難しいです。
デバイスやブラウザによって性能が異なるため、明確な数値はありません。 ハイエンドのスマートフォンではスムーズに動く同じページが、ローエンドのスマートフォンではクラッシュするかもしれません。
ここで重要なのは、RAILモデルを使って、ユーザーに焦点を当てることです。 ユーザーに人気のあるデバイスを調べ、それらのデバイスでページをテストしてみましょう。
Monitor memory use in realtime with the Chrome Task Manager #
メモリの問題を調査するための出発点として、Chrome タスク マネージャーを使用します。
-
Shift+Esc を押すか、Chrome のメイン メニューから [その他のツール] > [タスク マネージャ] を選択して、タスク マネージャを開きます。
-
タスク マネージャーのテーブル ヘッダーを右クリックし、JavaScript のメモリを有効にします。
これらの 2 つの列は、ページがどのようにメモリを使用しているかについて、異なることを教えてくれます:
- 「メモリ」列は、ネイティブ メモリを表します。 DOM ノードはネイティブ メモリに保存されます。 この値が増加している場合、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
x
x
配列にプッシュされます。 このコードを実行すると、次のスクリーンショットのようなタイムライン記録が生成されます:
まず、ユーザー インターフェイスについて説明します。 概要」ペインのHEAPグラフ(NET以下)は、JSのヒープを表しています。 概要」ペインの下には「カウンター」ペインがあります。 ここでは、メモリ使用量を、JSヒープ(概要ペインのHEAPグラフと同じ)、ドキュメント、DOMノード、リスナー、GPUメモリに分けて見ることができます。
さて、スクリーンショットと比較したコードの分析です。 ノード カウンター (緑のグラフ) を見ると、コードときれいに一致していることがわかります。 ノード数は離散的に増加しています。 ノード数が増加するたびに、grow()
が呼び出されていると推測できます。 JSヒープのグラフ(青のグラフ)は、それほど単純ではありません。 ベストプラクティスに則り、最初の傾斜は実際には強制的なガベージコレクションです(「Collect garbage」ボタンを押すことで実現します)。 記録が進むにつれ、JSヒープのサイズが急増しているのがわかります。 JavaScriptコードは、ボタンをクリックするたびにDOMノードを作成し、100万文字の文字列を作成する際に多くの作業を行っているからです。 ここで重要なのは、JSのヒープが開始時よりも終了時の方が高いという事実です(ここでいう「開始時」とは、強制的にガベージコレクションを行った後の時点です)。
Discover detached DOM tree memory leak with Heap Snapshots #
DOM ノードは、ページの DOM ツリーまたは JavaScript コードのいずれかからそのノードへの参照がない場合にのみ、ガベージ コレクションされます。 ノードが DOM ツリーから削除されても、一部の JavaScript がまだ参照している場合、ノードは「切り離された」といいます。 切り離された DOM ノードは、メモリ・リークの一般的な原因です。
以下に、切り離された 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
ノードが作成されます。
ヒープ スナップショットは、切り離されたノードを識別する 1 つの方法です。 その名のとおり、ヒープ スナップショットは、スナップショットの時点で、ページの JS オブジェクトと DOM ノードの間でメモリがどのように分散されているかを示します。
スナップショットを作成するには、DevTools を開いて [Profiles] パネルに移動し、[Take Heap Snapshot] ラジオ ボタンを選択し、[Take Snapshot] ボタンを押します。
スナップショットの処理と読み込みには時間がかかることがあります。
切り離された DOM ツリーを検索するには、Class filter のテキストボックスに Detached
と入力します。
切り離されたツリーを調査するためにカラットを展開します。
黄色くハイライトされたノードは、JavaScriptコードからの直接参照があります。 赤くハイライトされたノードは、直接の参照を持っていません。 黄色いノードのツリーの一部であるため、生きているだけです。 一般的には、黄色のノードに注目してください。 黄色いノードが必要以上に長く生きていないようにコードを修正し、黄色いノードのツリーの一部である赤いノードも取り除きます。 オブジェクト] ペインでは、そのノードを参照しているコードの詳細情報を見ることができます。 たとえば、以下のスクリーンショットでは、detachedTree
detachedTree
を使用しているコードを調べ、不要になったときにノードへの参照を削除するようにします。
Identify JS heap memory leak with Allocation Timelines #
アロケーション タイムラインは、JS ヒープのメモリ リークを追跡するのに役立つもう 1 つのツールです。
アロケーション タイムラインを示すために、次のコードを考えてみましょう。
var x = ;
function grow() {
x.push(new Array(1000000).join('x'));
}
document.getElementById('grow').addEventListener('click', grow);
コードで参照されているボタンが押されるたびに、100 万文字の文字列が x
配列に追加されます。
アロケーション タイムラインを記録するには、DevTools を開き、[Profiles] パネルで [Record Allocation Timeline] ラジオ ボタンを選択し、[Start] ボタンを押して、メモリ リークの原因と思われるアクションを実行し、終了したら [Stop Recording] ボタン () を押します。
記録しているときに、以下のスクリーンショットのように、アロケーション タイムライン上に青いバーが表示されているかどうかを確認してください。 これらの新しいメモリ割り当ては、メモリリークの候補です。
オブジェクトを拡大し、その値をクリックすると、[オブジェクト] ペインにその詳細が表示されます。 たとえば、以下のスクリーンショットでは、新しく割り当てられたオブジェクトの詳細を見ることで、そのオブジェクトがx
Window
変数に割り当てられていることがわかります。
Investigate memory allocation by function #
Record Allocation Profilerタイプを使用して、JavaScriptの関数ごとのメモリ割り当てを表示します。
- Record Allocation Profilerのラジオボタンを選択します。
- 開始ボタンを押します。
- 調査したいページ上のアクションを実行します。
- アクションがすべて終了したら停止ボタンを押します。
DevTools は関数ごとのメモリ割り当ての内訳を表示します。
Spot frequent garbage collections #
ページが頻繁に停止しているように見える場合は、ガベージ コレクションの問題がある可能性があります。
Chrome タスク マネージャーまたはタイムラインのメモリ記録を使用して、頻繁なガーベッジ コレクションを見つけることができます。
問題を特定したら、割り当てタイムラインの記録を使用して、どこでメモリが割り当てられているか、どの関数が割り当ての原因になっているかを調べることができます。