メモリの問題を解決する

Kayce Basques 氏
Kayce Basques

Chrome と DevTools を使用して、ページ パフォーマンスに影響するメモリの問題(メモリリーク、メモリの肥大化、頻繁なガベージ コレクションなど)を見つける方法について説明します。

概要

  • Chrome タスク マネージャーで、ページの現在のメモリ使用量を確認できます。
  • タイムラインの記録で、メモリ使用量の推移を可視化できます。
  • ヒープ スナップショットを使用して、デタッチされた DOM ツリー(メモリリークの一般的な原因)を特定します。
  • 割り当てタイムラインの記録を使用すると、JS ヒープに新しいメモリが割り当てられるタイミングを確認できます。

概要

RAIL パフォーマンス モデルの精神では、パフォーマンスへの取り組みはユーザーに焦点を当てる必要があります。

メモリの問題は、ユーザーが認識できることが多いため、重要です。ユーザーはメモリの問題を次のように認識できます。

  • 時間の経過とともにページのパフォーマンスが次第に低下する。これはメモリリークの兆候である可能性があります。メモリリークとは、ページのバグが原因で、時間の経過とともにページで使用されるメモリ量が徐々に増加することです。
  • ページのパフォーマンスが常に低い:これはメモリ肥大化の兆候である可能性があります。メモリが肥大化した状態とは、最適なページ速度を達成するために必要なメモリをページで使用する必要がある状態を指します。
  • ページのパフォーマンスが頻繁に遅れたり、一時停止しているように見える。これは、ガベージ コレクションが頻繁に実行される兆候である可能性があります。ガベージ コレクションとは、ブラウザがメモリを再利用することです。そのタイミングはブラウザが決定します。収集中は、すべてのスクリプトの実行が一時停止します。そのため、ブラウザがガベージ コレクションを頻繁に行うと、スクリプトの実行が何度も一時停止することになります。

メモリ肥大化: 「多すぎる」とは

メモリリークは簡単に定義できます。サイトのメモリ使用量が徐々に増えていれば、リークが発生しています。しかし、メモリが肥大化していると判断するのは少し困難です。「メモリ使用量が多すぎる」と判断される要因は何ですか?

デバイスやブラウザによって機能が異なるため、明確な数値はありません。ハイエンド スマートフォンでスムーズに動作するページが、ローエンド スマートフォンではクラッシュする可能性があります。

ここで重要なのは、RAIL モデルを使用して、ユーザーに焦点を当てることです。ユーザーに人気のあるデバイスを確認し、そのデバイスでページをテストします。ユーザー エクスペリエンスが常に好ましくない場合は、そのページがデバイスのメモリ能力を超過している可能性があります。

Chrome タスク マネージャーでメモリ使用量をリアルタイムでモニタリング

Chrome タスク マネージャーを出発点としてメモリの問題を調査します。タスク マネージャーは、ページで現在使用されているメモリ量を表示するリアルタイム モニターです。

  1. Shift+Esc キーを押すか、Chrome のメインメニューで [その他のツール] > [タスク マネージャー] を選択して、タスク マネージャーを開きます。

    タスク マネージャーを開く

  2. タスク マネージャーのテーブル ヘッダーを右クリックし、[JavaScript メモリ] を有効にします。

    JS メモリを有効にする

これら 2 つの列は、ページにおけるメモリの使用状況について、異なることを示しています。

  • [メモリ] 列はネイティブ メモリを表します。DOM ノードはネイティブ メモリに格納されます。この値が増えている場合、DOM ノードは作成中です。
  • [JavaScript メモリ] 列は JS ヒープを表します。この列には 2 つの値が含まれます。対象となる値は、ライブ番号(かっこ内の数字)です。ライブ数値は、ページ上の到達可能なオブジェクトが使用しているメモリ量を表します。この数値が増えている場合は、新しいオブジェクトが作成されているか、既存のオブジェクトが増加しています。

パフォーマンス記録でメモリリークを可視化する

[パフォーマンス] パネルを調査の出発点として使用することもできます。[パフォーマンス] パネルでは、ページのメモリ使用量の推移を可視化できます。

  1. DevTools で [パフォーマンス] パネルを開きます。
  2. [メモリ] チェックボックスをオンにします。
  3. 録画を行います

パフォーマンス メモリの記録のデモを行うために、次のコードを検討してください。

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 配列にプッシュされます。このコードを実行すると、次のスクリーンショットのような Timeline 記録が生成されます。

シンプルな成長の例

まず、ユーザー インターフェースについて説明します。[Overview] ペイン([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 ツリーを検索します。

接続解除されたノードのフィルタリング

カラットを展開して、デタッチされたツリーを調査します。

デタッチされたツリーの調査

黄色でハイライト表示されたノードは、JavaScript コードからの直接参照を示しています。赤色でハイライト表示されたノードには直接参照がありません。これらのノードは黄色のノードのツリーの一部であるため、存続しているだけです。通常は黄色のノードに注目します。黄色のノードが必要以上に存続するようにコードを修正し、黄色のノードのツリーに含まれる赤色のノードも削除します。

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

黄色のノードの調査

割り当てタイムラインを使用して JS ヒープのメモリリークを特定する

割り当てタイムラインは、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] ボタンを押し、メモリリークの原因と思われるアクションを実行してから、完了したら記録停止ボタン(録画停止ボタン)を押します。

記録中に、以下のスクリーンショットのように、割り当てタイムラインに青いバーが表示されます。

新しい割り当て

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

割り当てタイムラインの拡大

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

オブジェクトの詳細

関数別のメモリ割り当ての調査

JavaScript 関数別のメモリ割り当てを表示するには、[Memory] パネルの [Allocation Sampling] タイプを使用します。

レコード割り当てプロファイラ

  1. [Allocation Sampling] ラジオボタンを選択します。ページにワーカーが表示されている場合は、[Start] ボタンの横にあるプルダウン メニューを使用して、プロファイリング ターゲットとしてワーカーを選択できます。
  2. [Start] ボタンを押します。
  3. 調査対象のページで操作を行います。
  4. すべてのアクションが終了したら、停止ボタンを押します。

DevTools に、メモリ割り当ての内訳が関数別に表示されます。デフォルトのビューは [Heavy (Bottom Up)] で、最も多くのメモリを割り当てた関数が先頭に表示されます。

割り振りプロファイル

頻繁に実行されるガベージ コレクションを見つける

ページが頻繁に一時停止する場合は、ガベージ コレクションに問題がある可能性があります。

Chrome タスク マネージャーまたはタイムライン メモリの記録を使用して、頻繁に発生するガベージ コレクションを特定できます。タスク マネージャーで、メモリまたは JavaScript メモリの値が頻繁に増減する場合は、ガベージ コレクションが頻繁に行われていることを表します。タイムラインの記録で、JS ヒープまたはノード数のグラフの頻繁な増減は、ガベージ コレクションが頻繁に行われていることを示します。

問題を特定したら、割り当てタイムラインの記録を使用して、メモリが割り当てられている場所と、割り当てを引き起こしている関数を確認できます。