Tasuke Hubのロゴ

ITを中心に困っている人を助けるメディア

分かりやすく解決策を提供することで、あなたの困ったをサポート。 全ての人々がスムーズに生活できる世界を目指します。

JavaScriptのメモリリークを検出・修正する実践的な方法

記事のサムネイル

JavaScriptのメモリリークを検出・修正する実践的な方法

メモリリークとは何か

メモリリークとは、プログラムが使用したメモリが適切に解放されず、徐々にメモリ使用量が増加していく現象です。JavaScriptではガベージコレクション(GC)が自動的にメモリ管理を行いますが、特定の条件下では不要なメモリが解放されないことがあります。

// メモリリークの簡単な例
let leakedData = [];
function addData() {
  const largeData = new Array(1000000).fill('data');
  leakedData.push(largeData); // 配列への参照が残り続ける
}

// 何度も実行するとメモリが増加し続ける
setInterval(addData, 1000);

メモリリークが発生すると、以下のような問題が起きます:

  • アプリケーションのパフォーマンス低下
  • ブラウザのクラッシュ
  • ユーザー体験の悪化

よくあるメモリリークの原因

JavaScriptでメモリリークが発生する主な原因をコード例とともに解説します。

1. グローバル変数の誤用

// BAD: 意図しないグローバル変数
function createLeak() {
  accidentalGlobal = 'This is a leak'; // varやlet/constを付け忘れ
}

// GOOD: 適切なスコープで変数を宣言
function noLeak() {
  const localVariable = 'This is not a leak';
}

2. 削除されないイベントリスナー

// BAD: リスナーが削除されない
class Component {
  constructor() {
    this.handleClick = () => console.log(this);
    document.addEventListener('click', this.handleClick);
  }
  // removeEventListenerが呼ばれない
}

// GOOD: 適切にクリーンアップ
class Component {
  constructor() {
    this.handleClick = () => console.log(this);
    document.addEventListener('click', this.handleClick);
  }
  
  destroy() {
    document.removeEventListener('click', this.handleClick);
  }
}

3. タイマーの未解除

// BAD: setIntervalが解除されない
const timer = setInterval(() => {
  console.log(new Date());
}, 1000);

// GOOD: 適切にクリア
const timer = setInterval(() => {
  console.log(new Date());
}, 1000);

// 不要になったらクリア
clearInterval(timer);

Chrome DevToolsでメモリリークを検出する

Chrome DevToolsのメモリプロファイラを使用してメモリリークを検出する手順を解説します。

1. Heap Snapshotの使用方法

// テスト用のコード例
let leakyArray = [];

function createObjects() {
  for (let i = 0; i < 1000; i++) {
    leakyArray.push({
      data: new Array(1000).fill(Math.random())
    });
  }
}

// メモリプロファイラで確認するための操作
document.getElementById('create-btn').addEventListener('click', createObjects);

Heap Snapshotの手順:

  1. Chrome DevToolsを開く(F12)
  2. Memoryタブを選択
  3. "Take heap snapshot"をクリック
  4. 問題のある操作を実行
  5. 再度snapshotを取得
  6. 比較モードで差分を確認

2. Allocation Timelineの活用

Allocation Timelineを使うと、時系列でメモリの割り当てを確認できます。

// メモリリークを可視化しやすいコード
class DataManager {
  constructor() {
    this.cache = new Map();
  }
  
  addData(key, value) {
    // キャッシュが無限に増加する問題
    this.cache.set(key, {
      timestamp: Date.now(),
      data: new Array(10000).fill(value)
    });
  }
}

const manager = new DataManager();
// 1秒ごとに新しいデータを追加
let counter = 0;
setInterval(() => {
  manager.addData(`key_${counter++}`, 'test');
}, 1000);

実践的なメモリリーク修正方法

検出したメモリリークを修正する実践的な方法を紹介します。

1. WeakMapとWeakSetの活用

// BAD: 通常のMapは参照を保持
const cache = new Map();
function addToCache(obj, data) {
  cache.set(obj, data);
  // objが削除されてもcacheに残る
}

// GOOD: WeakMapは弱参照を使用
const cache = new WeakMap();
function addToCache(obj, data) {
  cache.set(obj, data);
  // objが削除されると自動的にcacheからも削除
}

2. クロージャーの適切な管理

// BAD: 大きなデータへの参照が残る
function createClosure() {
  const largeData = new Array(1000000).fill('data');
  
  return function() {
    // largeDataの一部しか使わないが全体が保持される
    console.log(largeData[0]);
  };
}

// GOOD: 必要な部分だけを参照
function createClosure() {
  const largeData = new Array(1000000).fill('data');
  const firstElement = largeData[0];
  
  return function() {
    console.log(firstElement);
  };
}

3. DOMノードの適切な削除

// BAD: DOMから削除してもJavaScript側に参照が残る
const elements = [];
function addElement() {
  const div = document.createElement('div');
  document.body.appendChild(div);
  elements.push(div); // 配列に参照を保持
  
  // 後でDOMから削除しても配列に残る
  div.remove();
}

// GOOD: 参照も削除する
function removeElement(element) {
  element.remove();
  const index = elements.indexOf(element);
  if (index > -1) {
    elements.splice(index, 1);
  }
}

メモリリークを防ぐベストプラクティス

メモリリークを未然に防ぐためのベストプラクティスを紹介します。

1. リソースの適切な管理パターン

// リソース管理のベストプラクティス
class ResourceManager {
  constructor() {
    this.resources = new Set();
  }
  
  acquire(resource) {
    this.resources.add(resource);
    return resource;
  }
  
  release(resource) {
    resource.cleanup?.();
    this.resources.delete(resource);
  }
  
  releaseAll() {
    this.resources.forEach(resource => {
      this.release(resource);
    });
    this.resources.clear();
  }
}

2. イベントリスナーの自動クリーンアップ

// 自動クリーンアップを実装したイベントマネージャー
class EventManager {
  constructor() {
    this.listeners = new Map();
  }
  
  addEventListener(element, event, handler) {
    if (!this.listeners.has(element)) {
      this.listeners.set(element, new Map());
    }
    
    const elementListeners = this.listeners.get(element);
    if (!elementListeners.has(event)) {
      elementListeners.set(event, new Set());
    }
    
    elementListeners.get(event).add(handler);
    element.addEventListener(event, handler);
  }
  
  removeAllListeners(element) {
    const elementListeners = this.listeners.get(element);
    if (elementListeners) {
      elementListeners.forEach((handlers, event) => {
        handlers.forEach(handler => {
          element.removeEventListener(event, handler);
        });
      });
      this.listeners.delete(element);
    }
  }
}

3. メモリ使用量の監視

// メモリ使用量を監視する簡単なユーティリティ
function monitorMemory(interval = 5000) {
  if (performance.memory) {
    setInterval(() => {
      const memInfo = performance.memory;
      console.log(`Memory Usage: ${(memInfo.usedJSHeapSize / 1048576).toFixed(2)} MB`);
      console.log(`Memory Limit: ${(memInfo.jsHeapSizeLimit / 1048576).toFixed(2)} MB`);
    }, interval);
  }
}

まとめ

JavaScriptのメモリリークは、アプリケーションのパフォーマンスとユーザー体験に大きな影響を与える重要な問題です。この記事で紹介した以下のポイントを実践することで、メモリリークを効果的に検出・修正できます。

重要なポイント:

  • メモリリークの主な原因(グローバル変数、イベントリスナー、タイマー)を理解する
  • Chrome DevToolsのメモリプロファイラを使った検出方法を習得する
  • WeakMapやWeakSetなどの適切なデータ構造を使用する
  • リソースの自動クリーンアップパターンを実装する

メモリ管理は継続的な改善プロセスです。定期的にメモリプロファイルを確認し、問題を早期に発見・修正することが重要です。本記事のテクニックを活用して、より効率的で安定したJavaScriptアプリケーションを開発してください。

TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

おすすめ記事

おすすめコンテンツ