【基礎編】Chromeデベロッパーツールのメモリタブとは?メモリ管理の基礎知識

Chrome DevToolsのMemoryタブの基礎を解説するイラスト。Heap snapshotやガベージコレクションなどのオプションが表示され、右上に『入門』と書かれている。

はじめに:こんな悩みはありませんか?

Webアプリケーションを開発していると、こんな問題に遭遇することがあります。

  • 「最初は快適だったのに、使っているうちにどんどん重くなる…」
  • 「長時間使っているとブラウザがクラッシュする」
  • 「メモリ使用量が増え続けて、動作が遅くなる」
  • 「『メモリリーク』って言われたけど、何をどう調べればいいの?」

これらの問題の多くは、メモリ管理に関係しています。

この記事では、Chromeデベロッパーツールのメモリタブを理解するための土台となる、メモリ管理の基礎知識を初心者にも分かりやすく解説します。


メモリとは何か?コンピュータの仕組みから理解しよう

メモリ(RAM)の役割

メモリ(RAM)は、コンピュータがプログラムやデータを一時的に保存する場所です。

例え話:

  • ハードディスク/SSD = 本棚(長期保存)
  • メモリ(RAM) = 作業机(一時的な作業スペース)
  • CPU = あなた(作業する人)

作業をするとき、本棚から本を取り出して机の上に広げますよね。机が広ければたくさんの本を同時に広げられますが、狭いと作業効率が落ちます。

メモリも同じで、容量が限られているため、不要なデータは適宜片付ける必要があります。

Webアプリケーションでのメモリ使用

ブラウザでWebページを開くと、以下のようなデータがメモリに読み込まれます:

  • HTML・CSS・JavaScript のコード
  • 画像・動画などのメディアファイル
  • API から取得したデータ
  • DOM ノード(ページの要素)
  • JavaScriptで作成したオブジェクト・配列

これらのデータが適切に管理されないと、メモリリークが発生します。


JavaScriptのメモリ管理の仕組み

自動メモリ管理とガベージコレクション

C言語などの低レベル言語では、開発者が手動でメモリを確保・解放する必要があります。しかし、JavaScriptは自動的にメモリを管理してくれます。

この自動メモリ管理の仕組みが、ガベージコレクション(Garbage Collection, GC)です。

メモリのライフサイクル

JavaScriptにおけるメモリは、以下の3つのステップで管理されます。

  1. 確保(Allocation):変数やオブジェクトを作成すると、メモリが割り当てられる
  2. 使用(Usage):割り当てられたメモリを読み書きする
  3. 解放(Deallocation):不要になったメモリをガベージコレクタが自動的に回収する
// 1. 確保:メモリが割り当てられる
let user = {
  name: '太郎',
  age: 28
};

// 2. 使用:メモリを読み書き
console.log(user.name); // '太郎'
user.age = 29;

// 3. 解放:user が不要になると自動的にメモリが解放される
user = null; // 明示的に参照を切る
// この後、ガベージコレクタがメモリを回収

ガベージコレクションの仕組み:Mark-and-Sweep

JavaScriptエンジン(Chromeの場合はV8)は、Mark-and-Sweepというアルゴリズムでメモリを管理します。

基本的な考え方:到達可能性(Reachability)

ガベージコレクタは、「ルート(root)から到達できるオブジェクトは必要、到達できないオブジェクトは不要」と判断します。

ルートとは:

  • グローバル変数(windowオブジェクトなど)
  • 現在実行中の関数のローカル変数
  • 呼び出しスタック上の関数の変数

Mark-and-Sweepの動作手順

ステップ1:Mark(マーク)フェーズ

  1. ガベージコレクタは、ルートから開始
  2. ルートから参照されているすべてのオブジェクトに「マーク」をつける
  3. マークされたオブジェクトから参照されているオブジェクトにも、再帰的にマークをつける

ステップ2:Sweep(スイープ)フェーズ

  1. メモリ内のすべてのオブジェクトをスキャン
  2. マークがついていないオブジェクトを「ゴミ(不要なもの)」と判断
  3. マークがないオブジェクトのメモリを解放

具体例で理解する

// グローバル変数(ルート)
let person1 = {
  name: '太郎',
  friend: null
};

let person2 = {
  name: '花子'
};

// person1 から person2 を参照
person1.friend = person2;

// この時点での状態:
// person1(ルートから到達可能) ✅
//  ↓ friend
// person2(person1から到達可能) ✅

// person1 への参照を削除
person1 = null;

// この時点での状態:
// person1 オブジェクト(到達不可能) ❌ → ガベージコレクト対象
// person2 オブジェクト(到達不可能) ❌ → ガベージコレクト対象

// ガベージコレクタが自動的に両方のメモリを回収

Generational Garbage Collection(世代別GC)

V8エンジンは、さらに高度な世代別ガベージコレクションを使用しています。

世代説明GCの頻度
Young Generation(新世代)新しく作られたオブジェクト頻繁(Minor GC)
Old Generation(旧世代)長く生き残ったオブジェクト低頻度(Major GC)

なぜ世代別にするのか?

多くのオブジェクトは、作成されてすぐに不要になります。例えば、関数内で一時的に作られた変数などです。そのため、新しいオブジェクトを頻繁にチェックし、長生きしているオブジェクトはあまりチェックしない方が効率的です。


メモリリークとは?なぜ起こるのか

メモリリークの定義

メモリリークとは、もう使わないはずのメモリが解放されず、メモリ使用量が増え続ける現象です。

症状:

  • ページの動作がだんだん遅くなる
  • ブラウザのメモリ使用量が増え続ける
  • 長時間使うとブラウザがクラッシュする
  • ページをリロードすると一時的に軽くなる

メモリリークが発生する主な原因

1. グローバル変数への不要な格納

// 悪い例:グローバル変数にデータを格納し続ける
window.userData = [];

function loadUser(user) {
  // ユーザーデータがどんどん溜まっていく
  window.userData.push(user);
}

// この関数を何度も呼ぶと、userData が肥大化
loadUser({ name: '太郎' });
loadUser({ name: '花子' });
// ...(何千回も呼ばれる)

解決策: 必要なデータだけを保持し、不要になったら削除する

2. イベントリスナーの削除忘れ

// 悪い例:イベントリスナーを削除しない
function addClickHandler() {
  const button = document.getElementById('myButton');

  button.addEventListener('click', function() {
    console.log('clicked!');
  });

  // ボタンが削除されても、イベントリスナーはメモリに残る
}

// 何度も呼ばれるとリスナーが溜まっていく
addClickHandler();
addClickHandler();
addClickHandler();

解決策: removeEventListener で明示的に削除する

// 良い例:イベントリスナーを削除する
function addClickHandler() {
  const button = document.getElementById('myButton');

  const handler = function() {
    console.log('clicked!');
  };

  button.addEventListener('click', handler);

  // 後で削除できるように参照を保持
  return function cleanup() {
    button.removeEventListener('click', handler);
  };
}

const cleanup = addClickHandler();
// 不要になったら削除
cleanup();

3. タイマー(setInterval)の停止忘れ

// 悪い例:setInterval を停止しない
function startPolling() {
  setInterval(function() {
    // APIを定期的に呼び出す
    fetch('/api/data').then(data => {
      console.log(data);
    });
  }, 5000);

  // この setInterval は永遠に動き続ける
}

startPolling();

解決策: clearInterval で停止する

// 良い例:setInterval を停止する
function startPolling() {
  const intervalId = setInterval(function() {
    fetch('/api/data').then(data => {
      console.log(data);
    });
  }, 5000);

  // 停止する関数を返す
  return function stopPolling() {
    clearInterval(intervalId);
  };
}

const stop = startPolling();
// 不要になったら停止
stop();

4. Detached DOM ノード(切り離されたDOM)

Detached DOMとは、ページから削除されたが、JavaScriptから参照されているため、メモリに残っているDOM要素のことです。

// 悪い例:削除したDOMへの参照を保持
let detachedNodes = [];

function createAndRemoveElement() {
  const div = document.createElement('div');
  div.innerHTML = 'Hello';
  document.body.appendChild(div);

  // 配列に保存
  detachedNodes.push(div);

  // DOMからは削除
  document.body.removeChild(div);

  // しかし、detachedNodes 配列がまだ参照しているため
  // メモリに残り続ける
}

// 何度も呼ばれると、削除されたDOMがメモリに溜まる
for (let i = 0; i < 1000; i++) {
  createAndRemoveElement();
}

解決策: DOM削除時に JavaScript の参照も削除する

5. クロージャによる意図しないメモリ保持

// 悪い例:クロージャが大きなデータを保持し続ける
function createHandler() {
  const largeData = new Array(1000000).fill('data'); // 大きな配列

  return function() {
    console.log(largeData[0]); // largeData全体が保持される
  };
}

const handler = createHandler();
// handler が存在する限り、largeData もメモリに残る

解決策: 必要なデータだけを保持する

// 良い例:必要なデータだけを保持
function createHandler() {
  const largeData = new Array(1000000).fill('data');
  const firstItem = largeData[0]; // 必要な部分だけ取り出す

  return function() {
    console.log(firstItem); // 小さなデータだけ保持
  };
}

Chromeデベロッパーツールのメモリタブでできること

ここまでメモリ管理の基礎を学びました。では、Chromeのメモリタブでは具体的に何ができるのでしょうか?

メモリタブの主要機能

機能用途
Heap Snapshot特定時点のメモリ使用状況を記録・比較
Allocation instrumentation on timeline時間経過とともにメモリがどう変化するか記録
Allocation samplingどの関数がメモリを多く使っているか調査
Detached elements切り離されたDOM要素を検出

1. Heap Snapshot(ヒープスナップショット)

用途: 特定のタイミングでのメモリ使用状況を「写真」のように記録します。

できること:

  • どのオブジェクトがメモリを占有しているか
  • オブジェクトのサイズと数
  • どこから参照されているか(Retainer)
  • 複数のスナップショットを比較して、メモリ増加の原因を特定

使うべきシーン:

  • 「このページ、メモリ使いすぎじゃない?」と思ったとき
  • 操作前後でメモリがどう変化したか確認したいとき

2. Allocation instrumentation on timeline

用途: 時間経過とともに、メモリがどのように割り当てられるかを記録します。

できること:

  • どのタイミングでメモリが増えているか
  • メモリ増加が止まらない場合の原因調査

使うべきシーン:

  • 「操作を繰り返すとメモリが増え続ける」というメモリリークの調査

3. Allocation sampling

用途: どのJavaScript関数がメモリを多く確保しているかを調査します。

できること:

  • パフォーマンスオーバーヘッドが少ない(長時間の記録に適している)
  • 関数ごとのメモリ割り当て量を確認

使うべきシーン:

  • 長時間動作するアプリケーションのメモリ使用量を調査したいとき

4. Detached elements

用途: JavaScriptから参照されているが、DOMツリーから削除された要素(Detached DOM)を検出します。

できること:

  • 削除されたはずのDOM要素がメモリに残っていないか確認
  • どのJavaScriptコードが参照を保持しているか特定

使うべきシーン:

  • SPA(Single Page Application)でページ遷移を繰り返すとメモリが増える場合

なぜメモリタブが重要なのか

1. ユーザー体験の向上

メモリリークがあると、ユーザーは以下のような問題を経験します:

  • ページがだんだん重くなる
  • 操作が遅くなる・反応しなくなる
  • ブラウザがクラッシュする

メモリタブを使って問題を発見・修正することで、快適なユーザー体験を提供できます。

2. 長時間稼働するアプリケーションの安定性

以下のようなアプリケーションは、特にメモリリークに注意が必要です:

  • ダッシュボード(常に開きっぱなしのアプリ)
  • リアルタイムチャット
  • 株価・為替などのリアルタイム更新サイト
  • 管理画面(長時間作業する)

メモリリークがあると、数時間後にクラッシュしてしまいます。

3. モバイルデバイスへの配慮

デスクトップPCと比べて、スマートフォンはメモリ容量が少ないです。そのため、メモリリークの影響がより顕著に現れます。

メモリ使用量を最適化することで、モバイルユーザーにも優しいアプリケーションを作れます。


まとめ:基礎を押さえたら実践へ!

この記事では、メモリタブを理解するための基礎知識を解説しました。

ポイントの振り返り

  • メモリ = プログラムやデータを一時的に保存する場所
  • ガベージコレクション = JavaScriptが自動的にメモリを管理する仕組み
  • Mark-and-Sweep = 到達可能なオブジェクトは残し、到達不可能なオブジェクトは削除
  • メモリリーク = 不要なメモリが解放されず、メモリ使用量が増え続ける現象
  • 主な原因 = グローバル変数、イベントリスナー削除忘れ、タイマー停止忘れ、Detached DOM、クロージャ
  • メモリタブ = メモリ使用状況を可視化し、メモリリークを特定できる強力なツール

次のステップ

基礎が理解できたら、次は実践編で実際の操作方法を学びましょう!

実践編では、以下の内容を詳しく解説します:

  • メモリタブの開き方と基本操作
  • Heap Snapshotの取得と分析方法
  • メモリリークの特定方法
  • 実務での解決事例(ビフォー・アフター)
  • よくあるハマりポイントと対処法
  • 実践的なコード例とデバッグ手順

【実践編】Chromeデベロッパーツールでメモリリークを特定する方法【実例付き】を読んで、メモリタブを使いこなせるようになりましょう!


参考資料: