メモリの問題を解決する

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

まとめ

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

概要

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

メモリの問題は、ユーザーが気付くことが多いため重要です。メモリの問題は以下のように認識されます。

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

メモリ肥大化: 「多すぎる」状態はどれくらいか

メモリリークは簡単に定義できます。サイトのメモリ使用量が次第に増えていれば、リークが発生します。しかし、メモリの肥大化の特定は少し困難です。「メモリ使用量が多すぎる」と判断するのは、どのようなことですか。

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

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

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

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

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

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

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

    JS メモリの有効化

次の 2 つの列は、ページでのメモリの使用状況に関するさまざまな情報を示します。

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

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

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

  1. DevTools で [Performance] パネルを開きます。
  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);

コード内で参照されているボタンが押されるたびに、1 万の div ノードがドキュメント本文に追加され、100 万文字の x 文字列が x 配列にプッシュされます。このコードを実行すると、次のスクリーンショットのようなタイムライン記録が生成されます。

単純な成長の例

まず、ユーザー インターフェースについて説明します。[概要] ペイン([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] ボタンを押します。

ヒープ スナップショットを取得

スナップショットの処理と読み込みには時間がかかることがあります。完了したら、左側のパネルで [HEAP SNAPSHOTS] を選択します。

[クラスフィルタ] テキスト ボックスに「Detached」と入力して、デタッチされた DOM ツリーを検索します。

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

カラットを開いて、分離されたツリーを調べます。

分離ツリーの調査

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

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

黄色のノードの調査

Allocation Timeline で JS ヒープのメモリリークを特定する

Allocation Timeline も、JS ヒープのメモリリークの追跡に役立つツールです。

割り当てタイムラインを説明するために、次のコードを考えてみましょう。

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] ボタン(録画を停止ボタン)を押します。

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

新しい割り当て

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

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

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

オブジェクトの詳細

関数ごとにメモリ割り当てを調査する

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

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

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

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

割り当てプロファイル

頻繁に行われるガベージ コレクションを特定する

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

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

問題を特定したら、Allocation Timeline 記録を使用して、メモリが割り当てられている場所と、割り当ての原因となっている関数を確認できます。