メモリの問題を解決する

Chrome と DevTools を使用して、メモリリーク、メモリ増加、頻繁なガベージ コレクションなど、ページのパフォーマンスに影響するメモリの問題を特定する方法を学びます。

概要

  • Chrome タスク マネージャーで、ページが使用しているメモリ量を確認します。
  • タイムライン レコーディングを使用して、時間の経過に伴うメモリ使用量を可視化します。
  • ヒープ スナップショットを使用して、分離された DOM ツリー(メモリリークの一般的な原因)を特定します。
  • 割り当てタイムラインの録画を使用して、JS ヒープ内に新しいメモリが割り当てられているタイミングを確認します。
  • JavaScript 参照によって保持されているデタッチされた要素を特定します。

概要

RAIL パフォーマンス モデルの精神に則り、パフォーマンス向上の取り組みはユーザーに焦点を当てるべきです。

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

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

メモリ使用量の増加: 「多すぎる」とはどの程度か

メモリリークは簡単に定義できます。サイトのメモリ使用量が徐々に増加している場合は、リークが発生しています。ただし、メモリ使用量の増加は特定が少し難しいです。「メモリ使用量が多すぎる」とはどのような状態ですか?

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

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

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

メモリに関する問題の調査を始める際は、Chrome タスク マネージャーを使用します。タスクマネージャーは、ページが使用しているメモリ量をリアルタイムで確認できるモニターです。

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

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

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

    タスク マネージャーのヘッダーで JS メモリを有効化。

これらの 2 つの列は、ページのメモリ使用方法について異なる情報を提供します。

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

    JavaScript メモリ ヘッダーが有効になっているタスク マネージャー。

パフォーマンス レコーディングでメモリリークを可視化する

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

  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);

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

簡単な成長の例。

まず、ユーザー インターフェースについて説明します。[概要] ペイン([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 ツリーには存在しないため、切断されます。

ヒープ スナップショットは、切断されたノードを特定する方法の一つです。名前が示すように、ヒープ スナップショットには、スナップショット時点のページの JS オブジェクトと DOM ノードのメモリ分布が表示されます。

スナップショットを作成するには、DevTools を開いて [メモリ] パネルに移動し、[ヒープ スナップショット] ラジオボタンを選択して、[スナップショットを取得] ボタンを押します。

[ヒープ スナップショットを取得] ラジオボタンが選択されています。

スナップショットの処理と読み込みに時間がかかることがあります。完了したら、左側のパネル([ヒープ スナップショット] という名前)から選択します。

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

切断されたノードのフィルタリング。

カレットを開いて、切断されたツリーを調査します。

分離されたツリーの調査。

ノードをクリックして詳細を調査します。[Objects] ペインには、そのオブジェクトを参照しているコードの詳細が表示されます。たとえば、次のスクリーンショットでは、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 を開き、[メモリ] パネルに移動して [タイムライン上の割り当て] ラジオボタンを選択し、 [記録] ボタンを押します。メモリリークの原因と思われる操作を行い、完了したら [録画を停止] ボタンを押します。

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

パフォーマンス タイムラインに新しいアロケーションが表示される。

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

ズームされた割り当てタイムライン。

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

文字列配列のオブジェクトの詳細。

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

[メモリ] パネルでプロファイル タイプ [割り当てサンプリング] を使用して、JavaScript 関数ごとのメモリ割り当てを表示します。

[メモリ] パネルの割り当てサンプリング プロファイラ。

  1. [割り当てサンプリング] ラジオボタンを選択します。ページにワーカーがある場合は、[JavaScript VM インスタンスを選択] ウィンドウでプロファイリング ターゲットとして選択できます。
  2. スタートボタンを押します。
  3. 調査するページでアクションを実行します。
  4. すべての操作が完了したら、[停止] ボタンを押します。

DevTools には、関数ごとのメモリ割り当ての内訳が表示されます。デフォルトのビューは [重い(ボトムアップ)] で、最も多くのメモリを割り当てた関数が上部に表示されます。

アロケーション プロファイルの結果ページ。

JS 参照によって保持されているオブジェクトを特定する

[デタッチされた要素] プロファイルには、JavaScript コードによって参照されているため保持されるデタッチされた要素が表示されます。

Detached elements プロファイルを記録して、正確な HTML ノードとノード数を表示します。

デタッチされた要素のプロファイルの例。

頻繁なガベージ コレクションを検出する

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

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

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