Chrome と DevTools を使用して、メモリリーク、メモリ肥大化、頻繁なガベージ コレクションなど、ページのパフォーマンスに影響するメモリの問題を特定する方法を説明します。
概要
- Chrome タスク マネージャーを使用して、ページが使用しているメモリ量を確認します。
- タイムラインの記録を使用して、メモリ使用量の推移を可視化します。
- ヒープ スナップショットを使用して、デタッチされた DOM ツリー(メモリリークの一般的な原因)を特定します。
- 割り当てタイムラインの記録を使用して、JS ヒープに新しいメモリが割り当てられるタイミングを確認します。
- JavaScript 参照によって保持されているデタッチされた要素を特定する。
概要
RAIL パフォーマンス モデルの精神に基づき、パフォーマンスの取り組みの焦点はユーザーに置く必要があります。
メモリの問題は、ユーザーが認識できることが多いため、重要です。ユーザーは、次の方法でメモリの問題を認識できます。
- ページのパフォーマンスが時間の経過とともに徐々に低下する。これは、メモリリークの症状である可能性があります。メモリリークとは、ページ内のバグが原因で、時間の経過とともにページが使用するメモリが徐々に増えていく現象です。
- ページのパフォーマンスが常に悪い。これはメモリ肥大化の症状である可能性があります。メモリの肥大化とは、ページが最適な Page Speed に必要なメモリよりも多くのメモリを使用している状態です。
- ページのパフォーマンスが遅延する、または頻繁に一時停止しているように見える。これは、ガベージ コレクションが頻繁に発生していることが原因である可能性があります。ガベージ コレクションとは、ブラウザがメモリを再利用することです。このタイミングはブラウザが決定します。コレクション中は、すべてのスクリプトの実行が一時停止されます。そのため、ブラウザがガベージ コレクションを頻繁に行うと、スクリプトの実行が頻繁に一時停止されます。
メモリの肥大化: 「多すぎる」とはどの程度か
メモリリークの定義は簡単です。サイトが使用するメモリが徐々に増えている場合は、リークが発生しています。しかし、メモリの肥大化を特定するのは少し困難です。「メモリ使用量が多すぎる」とはどのような状態ですか?
デバイスやブラウザによって機能が異なるため、具体的な数値はありません。ハイエンドのスマートフォンではスムーズに動作するページが、ローエンドのスマートフォンではクラッシュすることがあります。
ここで重要なのは、RAIL モデルを使用してユーザーに焦点を当てることです。ユーザーに人気のデバイスを特定し、それらのデバイスでページをテストします。エクスペリエンスが常に悪い場合、ページがそれらのデバイスのメモリ容量を超えている可能性があります。
Chrome タスク マネージャーでメモリ使用量をリアルタイムでモニタリングする
メモリの問題の調査を開始するには、Chrome タスク マネージャーを使用します。タスク マネージャーは、ページが使用しているメモリ量をリアルタイムで表示するモニターです。
Shift+Esc キーを押すか、Chrome のメインメニューに移動して [その他のツール] > [タスク マネージャー] を選択して、タスク マネージャーを開きます。

タスク マネージャーの表の見出しを右クリックし、[JavaScript メモリ] を有効にします。

この 2 つの列は、ページがメモリをどのように使用しているかについて、異なる情報を示しています。
- [メモリ使用量] 列は OS メモリを表します。DOM ノードは OS メモリに保存されます。この値が増加している場合は、DOM ノードが作成されています。
[JavaScript Memory] 列は JS ヒープを表します。この列には 2 つの値が含まれます。関心のある値は、ライブ数(括弧内の数値)です。ライブ数は、ページ上の到達可能なオブジェクトが使用しているメモリ量を示します。この数が増加している場合は、新しいオブジェクトが作成されているか、既存のオブジェクトが増加しています。

パフォーマンスの記録でメモリリークを可視化する
調査の別の出発点として、[パフォーマンス] パネルを使用することもできます。[Performance] パネルを使用すると、ページのメモリ使用量の推移を可視化できます。
- DevTools で [Performance] パネルを開きます。
- [メモリ] チェックボックスをオンにします。
- 録音する。
Performance メモリ記録を実証するため、次のコードを考えてみましょう。
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);
コードで参照されているボタンが押されるたびに、10,000 個の div ノードがドキュメント本文に追加され、100 万個の x 文字の文字列が x 配列に push されます。このコードを実行すると、次のスクリーンショットのようなタイムライン記録が生成されます。

まず、ユーザー インターフェースについて説明します。[Overview] ペイン([NET] の下)の [HEAP] グラフは、JS ヒープを表しています。[概要] ペインの下に [カウンタ] ペインがあります。ここでは、JS ヒープ([Overview] ペインの [HEAP] グラフと同じ)、ドキュメント、DOM ノード、リスナー、GPU メモリに分類されたメモリ使用量を確認できます。チェックボックスをオフにすると、グラフに表示されなくなります。
次に、コードとスクリーンショットを比較した分析です。ノードカウンタ(緑色のグラフ)を見ると、コードと完全に一致していることがわかります。ノード数は個別のステップで増加します。ノード数の増加は、grow() の呼び出しと見なすことができます。JS ヒープグラフ(青色のグラフ)は、それほど単純ではありません。ベスト プラクティスに沿って、最初の低下は強制的なガベージ コレクション([collect garbage] ボタンを押すことで実現)です。記録が進むにつれて、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 ツリーに存在しないため、デタッチされています。
ヒープ スナップショットは、デタッチされたノードを特定する方法の一つです。名前が示すように、ヒープ スナップショットは、スナップショットの時点でページの JS オブジェクトと DOM ノード間でメモリがどのように分散されているかを示します。
スナップショットを作成するには、DevTools を開いて [Memory] パネルに移動し、[Heap Snapshot] ラジオボタンを選択して、[Take snapshot] ボタンを押します。
![[Take heap snapshot] ラジオボタンが選択されています。](https://developer.chrome.google.cn/static/docs/devtools/memory-problems/image/take-heap-snapshot.png?hl=ja)
スナップショットの処理と読み込みには時間がかかることがあります。完了したら、左側のパネル([ヒープ スナップショット])から選択します。
[Class filter] 入力ボックスに「Detached」と入力して、デタッチされた DOM ツリーを検索します。

キャレットを開いて、デタッチされたツリーを調査します。

ノードをクリックして詳細を調査します。[オブジェクト] ペインで、それを参照しているコードの詳細を確認できます。たとえば、次のスクリーンショットでは、detachedTree 変数がノードを参照していることがわかります。この特定のメモリリークを修正するには、detachedTree を使用するコードを調べて、不要になったノードへの参照が削除されるようにします。

割り当てタイムラインを使用して JS ヒープメモリ リークを特定する
Allocation Timeline は、JS ヒープ内のメモリリークを追跡するのに役立つもう 1 つのツールです。
割り当てタイムラインを説明するために、次のコードについて考えてみましょう。
var x = [];
function grow() {
x.push(new Array(1000000).join('x'));
}
document.getElementById('grow').addEventListener('click', grow);
コードで参照されているボタンが押されるたびに、100 万文字の文字列が x 配列に追加されます。
割り当てタイムラインを記録するには、DevTools を開き、[Memory] パネルに移動して、[Allocations on timeline] ラジオボタンを選択し、 [Record] ボタンを押して、メモリリークの原因と思われる操作を実行します。完了したら、 [Stop recording] ボタンを押します。
記録中に、次のスクリーンショットのように、割り当てタイムラインに青いバーが表示されるかどうかを確認します。

青いバーは新しいメモリ割り当てを表しています。これらの新しいメモリ割り当ては、メモリリークの候補です。バーをズームすると、[Constructor] ペインをフィルタして、指定した期間に割り当てられたオブジェクトのみを表示できます。

オブジェクトを開いてその値をクリックすると、[Object] ペインにそのオブジェクトの詳細が表示されます。たとえば、下のスクリーンショットでは、新しく割り当てられたオブジェクトの詳細を表示することで、Window スコープの x 変数に割り当てられたことがわかります。

関数ごとにメモリ割り当てを調査する
[メモリ] パネルの [割り当てサンプリング] プロファイル タイプを使用して、JavaScript 関数によるメモリ割り当てを表示します。
![[Memory] パネルの割り当てサンプリング プロファイラ。](https://developer.chrome.google.cn/static/docs/devtools/memory-problems/image/record-allocation-profile.png?hl=ja)
- [割り当てサンプリング] ラジオボタンを選択します。ページにワーカーがある場合は、[Select JavaScript VM instance] ウィンドウからプロファイリング ターゲットとして選択できます。
- スタートボタンを押します。
- 調査するページでアクションを実行します。
- すべてのアクションが完了したら、[Stop] ボタンを押します。
DevTools には、関数ごとのメモリ割り当ての内訳が表示されます。デフォルトのビューは [Heavy (Bottom Up)] で、最も多くのメモリを割り当てた関数が上部に表示されます。

JS 参照によって保持されているオブジェクトを特定する
[Detached elements] プロファイルには、JavaScript コードによって参照されているために保持されているデタッチされた要素が表示されます。
[Detached elements] プロファイルを記録して、正確な HTML ノードとノード数を表示します。

頻繁なガベージ コレクションを特定する
ページが頻繁に一時停止しているように見える場合は、ガベージ コレクションに問題がある可能性があります。
Chrome タスク マネージャーまたはタイムラインのメモリ記録を使用して、頻繁なガベージ コレクションを特定できます。タスク マネージャーで、[メモリ] または [JavaScript メモリ] の値が頻繁に増減している場合は、ガベージ コレクションが頻繁に実行されていることを示しています。タイムラインの記録で、JS ヒープまたはノード数のグラフが頻繁に上下している場合は、ガベージ コレクションが頻繁に発生していることを示しています。
問題を特定したら、割り当てタイムラインの記録を使用して、メモリが割り当てられている場所と、割り当ての原因となっている関数を特定できます。