ブログ記事

JavaScriptメモリ管理完全ガイド2025 - メモリリークの検出から最適化まで

JavaScriptのメモリ管理の仕組みを完全解説。ガベージコレクション、メモリリークの検出方法、Chrome DevToolsでのプロファイリング、実践的な最適化テクニックを詳しく紹介します。

20分で読めます
R
Rina
Daily Hack 編集長
プログラミング
JavaScript メモリ管理 パフォーマンス Chrome DevTools ガベージコレクション
JavaScriptメモリ管理完全ガイド2025 - メモリリークの検出から最適化までのヒーロー画像

JavaScriptのメモリ管理は、高パフォーマンスな Web アプリケーションを構築する上で極めて重要な要素です。本記事では、JavaScriptエンジンがどのようにメモリを管理し、開発者がどのようにメモリリークを防ぎ、アプリケーションを最適化できるかを包括的に解説します。

この記事で学べること

  • JavaScriptのメモリアロケーションの仕組み
  • ガベージコレクションアルゴリズムの詳細
  • よくあるメモリリークパターンと対処法
  • Chrome DevTools を使用した実践的なプロファイリング
  • プロダクション環境での最適化テクニック

JavaScriptのメモリライフサイクル

JavaScriptにおけるメモリ管理は、主に 3 つのフェーズで構成されています。

メモリの割り当て

変数宣言、オブジェクト生成時に必要なメモリを確保

メモリの使用

割り当てられたメモリの読み書き操作

メモリの解放

不要になったメモリをガベージコレクションで自動解放

メモリアロケーションの詳細

JavaScriptでは、以下のような場面でメモリが割り当てられます:

// プリミティブ値の割り当て(スタックメモリ)
let number = 42;              // 8バイト
let string = "Hello World";   // 文字列長に応じたメモリ
let boolean = true;           // 1バイト

// オブジェクトの割り当て(ヒープメモリ)
let object = {                // オブジェクト全体がヒープに
  name: "JavaScript",
  version: "ES2025"
};

// 配列の割り当て
let array = [1, 2, 3, 4, 5];  // 要素数に応じたヒープメモリ

// 関数の割り当て
function calculate() {        // 関数もヒープに格納
  return 42;
}

メモリ割り当ての最適化

大量のデータを扱う場合は、一度に大きなメモリを確保するよりも、必要に応じて段階的に割り当てることで、メモリフラグメンテーションを防ぐことができます。

ガベージコレクションの仕組み

V8 エンジンを例に、モダンな JavaScriptエンジンのガベージコレクション(GC)について詳しく見ていきましょう。

Mark and Sweep アルゴリズム

Mark and Sweep アルゴリズムの動作

チャートを読み込み中...

世代別ガベージコレクション

V8 では、オブジェクトを「新世代」と「旧世代」に分けて管理します:

V8の世代別メモリ管理
世代 特徴 GC頻度 メモリサイズ
新世代(Young Generation) 短命なオブジェクト 高頻度(数ミリ秒) 1-8MB
旧世代(Old Generation) 長寿命なオブジェクト 低頻度(数秒) 数百MB〜数GB

よくあるメモリリークパターン

1. グローバル変数の不適切な使用

// グローバルスコープの汚染
function loadUserData() {
  // varを使わず宣言 → グローバル変数に
  userData = fetchUserFromAPI();
  tempData = processData(userData);
}

// イベントリスナーの解除忘れ
element.addEventListener('click', function() {
  // 大量のデータ処理
});
// 適切なスコープ管理
function loadUserData() {
  const userData = fetchUserFromAPI();
  const tempData = processData(userData);
  return { userData, tempData };
}

// イベントリスナーの適切な管理
const clickHandler = function() {
  // 大量のデータ処理
};
element.addEventListener('click', clickHandler);
// 不要になったら解除
element.removeEventListener('click', clickHandler);
メモリリークが発生するコード
// グローバルスコープの汚染
function loadUserData() {
  // varを使わず宣言 → グローバル変数に
  userData = fetchUserFromAPI();
  tempData = processData(userData);
}

// イベントリスナーの解除忘れ
element.addEventListener('click', function() {
  // 大量のデータ処理
});
改善されたコード
// 適切なスコープ管理
function loadUserData() {
  const userData = fetchUserFromAPI();
  const tempData = processData(userData);
  return { userData, tempData };
}

// イベントリスナーの適切な管理
const clickHandler = function() {
  // 大量のデータ処理
};
element.addEventListener('click', clickHandler);
// 不要になったら解除
element.removeEventListener('click', clickHandler);

2. 循環参照によるメモリリーク

// DOM要素とJavaScriptオブジェクトの循環参照
class Component {
  constructor(element) {
    this.element = element;
    // DOM要素からこのインスタンスを参照
    element.component = this;
    
    // イベントリスナーでの参照
    element.addEventListener('click', () => {
      this.handleClick();
    });
  }
  
  destroy() {
    // 循環参照を解除
    delete this.element.component;
    // イベントリスナーも解除
    this.element.removeEventListener('click', this.handleClick);
    this.element = null;
  }
}

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

function createDataProcessor() {
  const hugeData = new Array(1000000).fill('data');
  
  return function process(item) {
    // hugeData全体が保持される
    return hugeData.includes(item);
  };
}
function createDataProcessor() {
  const hugeData = new Array(1000000).fill('data');
  // 必要な部分だけを抽出
  const dataSet = new Set(hugeData);
  
  return function process(item) {
    // Setだけが保持される
    return dataSet.has(item);
  };
}
メモリリークのリスク
function createDataProcessor() {
  const hugeData = new Array(1000000).fill('data');
  
  return function process(item) {
    // hugeData全体が保持される
    return hugeData.includes(item);
  };
}
最適化されたコード
function createDataProcessor() {
  const hugeData = new Array(1000000).fill('data');
  // 必要な部分だけを抽出
  const dataSet = new Set(hugeData);
  
  return function process(item) {
    // Setだけが保持される
    return dataSet.has(item);
  };
}

Chrome DevToolsでのメモリプロファイリング

ヒープスナップショットの取得と分析

// メモリリークを検出するためのテストコード
class MemoryLeakTest {
  constructor() {
    this.data = new Array(10000).fill('test');
    this.callbacks = [];
  }
  
  addCallback(fn) {
    this.callbacks.push(fn);
  }
  
  // リークを引き起こすメソッド
  createLeak() {
    const self = this;
    setInterval(() => {
      self.data.push(new Date());
    }, 100);
  }
}

// DevToolsでの分析手順
// 1. Performance タブでレコーディング開始
// 2. メモリ使用量の推移を観察
// 3. Heap Snapshotを複数回取得
// 4. 差分を比較してリークを特定

メモリプロファイリングのベストプラクティス

Heap Snapshot の活用

  • 特定時点のメモリ状態を詳細に分析
  • オブジェクトの参照関係を追跡
  • メモリリークの原因を特定
// スナップショット間の比較
// Snapshot 1: 初期状態
// Snapshot 2: 操作後
// Snapshot 3: GC後
// 差分でリーク対象を特定

Allocation Timeline の使用

  • リアルタイムでメモリ割り当てを監視
  • どの関数がメモリを消費しているか特定
  • メモリスパイクの原因を調査
// メモリ割り当ての追跡
performance.measureUserAgentSpecificMemory()
  .then(result => {
    console.log('Total memory:', result.bytes);
    console.log('Breakdown:', result.breakdown);
  });

Performance Monitor の設定

  • CPU 使用率とメモリ使用量を同時監視
  • 長時間のメモリトレンド分析
  • メモリプレッシャーの検出
// パフォーマンス監視の実装
const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  entries.forEach(entry => {
    if (entry.entryType === 'measure') {
      console.log(`${entry.name}: ${entry.duration}ms`);
    }
  });
});
observer.observe({ entryTypes: ['measure'] });

実践的な最適化テクニック

1. オブジェクトプーリングの実装

class ObjectPool {
  constructor(createFn, resetFn, maxSize = 100) {
    this.createFn = createFn;
    this.resetFn = resetFn;
    this.pool = [];
    this.maxSize = maxSize;
  }
  
  acquire() {
    if (this.pool.length > 0) {
      return this.pool.pop();
    }
    return this.createFn();
  }
  
  release(obj) {
    if (this.pool.length < this.maxSize) {
      this.resetFn(obj);
      this.pool.push(obj);
    }
  }
}

// 使用例:パーティクルシステム
const particlePool = new ObjectPool(
  () => ({ x: 0, y: 0, velocity: { x: 0, y: 0 }, active: false }),
  (particle) => {
    particle.x = 0;
    particle.y = 0;
    particle.velocity.x = 0;
    particle.velocity.y = 0;
    particle.active = false;
  }
);

2. WeakMapとWeakSetの活用

// DOM要素に関連するメタデータの管理
const elementMetadata = new WeakMap();

function attachMetadata(element, data) {
  elementMetadata.set(element, data);
}

function getMetadata(element) {
  return elementMetadata.get(element);
}

// DOM要素が削除されると自動的にメタデータも削除される

3. 遅延読み込みとメモリ管理

class LazyImageLoader {
  constructor() {
    this.observer = new IntersectionObserver(
      this.handleIntersection.bind(this),
      { rootMargin: '50px' }
    );
    this.loadedImages = new WeakSet();
  }
  
  observe(img) {
    if (this.loadedImages.has(img)) return;
    this.observer.observe(img);
  }
  
  handleIntersection(entries) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        this.loadedImages.add(img);
        this.observer.unobserve(img);
      }
    });
  }
}
メモリ最適化の効果 85 %

プロダクション環境でのメモリ監視

// メモリ使用量の定期的な監視
class MemoryMonitor {
  constructor(threshold = 100 * 1024 * 1024) { // 100MB
    this.threshold = threshold;
    this.interval = null;
  }
  
  start() {
    this.interval = setInterval(async () => {
      if (performance.memory) {
        const used = performance.memory.usedJSHeapSize;
        const total = performance.memory.totalJSHeapSize;
        
        if (used > this.threshold) {
          console.warn(`High memory usage: ${(used / 1024 / 1024).toFixed(2)}MB`);
          // アラートを送信
          this.sendAlert({
            used,
            total,
            timestamp: new Date().toISOString()
          });
        }
      }
    }, 30000); // 30秒ごと
  }
  
  stop() {
    clearInterval(this.interval);
  }
  
  sendAlert(data) {
    // 監視システムへの通知
    fetch('/api/alerts/memory', {
      method: 'POST',
      body: JSON.stringify(data)
    });
  }
}

メモリ管理の注意点

メモリ最適化は重要ですが、過度な最適化は可読性を損なう可能性があります。 実際のメモリ使用状況を測定し、本当に必要な箇所にのみ最適化を適用しましょう。

トラブルシューティング

メモリリークの特定方法

// 1. メモリリークの兼典的なパターン

// イベントリスナーの解除忘れ
class ComponentWithLeak {
  constructor() {
    this.data = new Array(1000000).fill('data');
    // リーク: アロー関数がthisを参照
    window.addEventListener('resize', () => {
      this.handleResize();
    });
  }
  
  handleResize() {
    console.log(this.data.length);
  }
  
  // 解決: 明示的なクリーンアップ
  destroy() {
    this.boundResize = this.handleResize.bind(this);
    window.removeEventListener('resize', this.boundResize);
    this.data = null;
  }
}

// 2. メモリリーク検出ツール
class MemoryLeakDetector {
  constructor() {
    this.snapshots = [];
    this.threshold = 50 * 1024 * 1024; // 50MB
  }
  
  async takeSnapshot() {
    if (!performance.memory) {
      console.warn('performance.memory is not available');
      return;
    }
    
    const snapshot = {
      timestamp: Date.now(),
      usedJSHeapSize: performance.memory.usedJSHeapSize,
      totalJSHeapSize: performance.memory.totalJSHeapSize,
      jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
    };
    
    this.snapshots.push(snapshot);
    
    // メモリ使用量の増加を検出
    if (this.snapshots.length > 10) {
      const oldSnapshot = this.snapshots[this.snapshots.length - 10];
      const increase = snapshot.usedJSHeapSize - oldSnapshot.usedJSHeapSize;
      
      if (increase > this.threshold) {
        console.error(`Memory leak detected! Increase: ${(increase / 1024 / 1024).toFixed(2)}MB`);
        this.generateReport();
      }
    }
  }
  
  generateReport() {
    const report = {
      startMemory: this.snapshots[0],
      currentMemory: this.snapshots[this.snapshots.length - 1],
      trend: this.calculateTrend(),
      recommendations: this.getRecommendations()
    };
    
    console.table(report);
    return report;
  }
  
  calculateTrend() {
    if (this.snapshots.length < 2) return 'insufficient data';
    
    const deltas = [];
    for (let i = 1; i < this.snapshots.length; i++) {
      deltas.push(this.snapshots[i].usedJSHeapSize - this.snapshots[i-1].usedJSHeapSize);
    }
    
    const avgDelta = deltas.reduce((a, b) => a + b) / deltas.length;
    
    if (avgDelta > 1024 * 1024) return 'increasing'; // 1MB以上の増加
    if (avgDelta < -1024 * 1024) return 'decreasing';
    return 'stable';
  }
  
  getRecommendations() {
    const trend = this.calculateTrend();
    const recommendations = [];
    
    if (trend === 'increasing') {
      recommendations.push('イベントリスナーの解除を確認');
      recommendations.push('循環参照がないかチェック');
      recommendations.push('大きなデータ構造の保持を見直す');
      recommendations.push('WeakMap/WeakSetの使用を検討');
    }
    
    return recommendations;
  }
}

// 使用例
const detector = new MemoryLeakDetector();
setInterval(() => detector.takeSnapshot(), 5000);

パフォーマンス問題の診断

// 1. ガベージコレクションの最適化

// GCフレンドリーなコードパターン
class GCOptimizedClass {
  constructor() {
    // オブジェクトプールの活用
    this.objectPool = [];
    this.poolSize = 100;
    
    // 事前割り当て
    this.preallocateObjects();
  }
  
  preallocateObjects() {
    for (let i = 0; i < this.poolSize; i++) {
      this.objectPool.push(this.createObject());
    }
  }
  
  createObject() {
    return {
      id: 0,
      data: null,
      active: false,
      reset() {
        this.id = 0;
        this.data = null;
        this.active = false;
      }
    };
  }
  
  getObject() {
    const obj = this.objectPool.pop() || this.createObject();
    obj.active = true;
    return obj;
  }
  
  releaseObject(obj) {
    obj.reset();
    if (this.objectPool.length < this.poolSize) {
      this.objectPool.push(obj);
    }
  }
}

// 2. メモリアロケーションの監視
class AllocationMonitor {
  constructor() {
    this.allocations = new Map();
    this.startTime = performance.now();
  }
  
  trackAllocation(type, size) {
    if (!this.allocations.has(type)) {
      this.allocations.set(type, {
        count: 0,
        totalSize: 0,
        instances: new WeakSet()
      });
    }
    
    const stats = this.allocations.get(type);
    stats.count++;
    stats.totalSize += size;
  }
  
  getReport() {
    const duration = performance.now() - this.startTime;
    const report = [];
    
    this.allocations.forEach((stats, type) => {
      report.push({
        type,
        count: stats.count,
        totalSize: stats.totalSize,
        avgSize: stats.totalSize / stats.count,
        allocationsPerSecond: (stats.count / duration) * 1000
      });
    });
    
    return report.sort((a, b) => b.totalSize - a.totalSize);
  }
}

// 3. メモリ効率的なデータ構造
class MemoryEfficientDataStructure {
  constructor() {
    // TypedArrayの使用(通常の配列よりメモリ効率的)
    this.intBuffer = new Int32Array(10000);
    this.floatBuffer = new Float32Array(10000);
    
    // ビットフラグでメモリ節約
    this.flags = new Uint8Array(10000);
  }
  
  // 複数の値を一つの数値にパック
  packData(x, y, z, w) {
    // 32ビットに4つの8ビット値をパック
    return (x & 0xFF) | ((y & 0xFF) << 8) | ((z & 0xFF) << 16) | ((w & 0xFF) << 24);
  }
  
  unpackData(packed) {
    return {
      x: packed & 0xFF,
      y: (packed >> 8) & 0xFF,
      z: (packed >> 16) & 0xFF,
      w: (packed >> 24) & 0xFF
    };
  }
}

Chrome DevTools の高度な活用

// 1. Performance APIを使ったカスタムマーカー
class PerformanceProfiler {
  constructor() {
    this.marks = new Map();
    this.measures = [];
  }
  
  startMeasure(name) {
    performance.mark(`${name}-start`);
    this.marks.set(name, performance.now());
  }
  
  endMeasure(name) {
    performance.mark(`${name}-end`);
    
    try {
      performance.measure(
        name,
        `${name}-start`,
        `${name}-end`
      );
      
      const duration = performance.now() - this.marks.get(name);
      this.measures.push({ name, duration });
      
      // DevToolsのパフォーマンスタブに表示
      console.log(`%c${name}: ${duration.toFixed(2)}ms`, 'color: blue');
      
    } catch (e) {
      console.error(`Failed to measure ${name}:`, e);
    }
  }
  
  // メモリスナップショットの自動取得
  async captureHeapSnapshot() {
    if ('memory' in performance && 'measureUserAgentSpecificMemory' in performance) {
      try {
        const measurement = await performance.measureUserAgentSpecificMemory();
        console.log('Memory measurement:', measurement);
        return measurement;
      } catch (e) {
        console.error('Memory measurement failed:', e);
      }
    }
  }
  
  // カスタムDevToolsプロトコル
  enableCustomProtocol() {
    if (typeof chrome !== 'undefined' && chrome.devtools) {
      chrome.devtools.panels.create(
        "Memory Profiler",
        null,
        "panel.html",
        function(panel) {
          panel.onShown.addListener(() => {
            this.startProfiling();
          });
        }
      );
    }
  }
}

// 2. メモリプロファイリングの自動化
class AutomatedMemoryProfiler {
  constructor(options = {}) {
    this.interval = options.interval || 60000; // 1分
    this.maxSnapshots = options.maxSnapshots || 10;
    this.snapshots = [];
  }
  
  start() {
    this.intervalId = setInterval(() => {
      this.takeSnapshot();
    }, this.interval);
  }
  
  async takeSnapshot() {
    const snapshot = {
      timestamp: new Date().toISOString(),
      memory: performance.memory ? {
        usedJSHeapSize: performance.memory.usedJSHeapSize,
        totalJSHeapSize: performance.memory.totalJSHeapSize,
        jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
      } : null,
      activeHandlers: this.countActiveHandlers(),
      domNodes: document.getElementsByTagName('*').length
    };
    
    this.snapshots.push(snapshot);
    
    if (this.snapshots.length > this.maxSnapshots) {
      this.snapshots.shift();
    }
    
    this.analyzeSnapshots();
  }
  
  countActiveHandlers() {
    // イベントリスナーの数を推定
    let count = 0;
    const elements = document.querySelectorAll('*');
    
    elements.forEach(el => {
      const listeners = getEventListeners ? getEventListeners(el) : {};
      Object.keys(listeners).forEach(type => {
        count += listeners[type].length;
      });
    });
    
    return count;
  }
  
  analyzeSnapshots() {
    if (this.snapshots.length < 2) return;
    
    const latest = this.snapshots[this.snapshots.length - 1];
    const previous = this.snapshots[this.snapshots.length - 2];
    
    if (latest.memory && previous.memory) {
      const memoryDelta = latest.memory.usedJSHeapSize - previous.memory.usedJSHeapSize;
      const domDelta = latest.domNodes - previous.domNodes;
      
      if (memoryDelta > 5 * 1024 * 1024) { // 5MB以上の増加
        console.warn('メモリ使用量が急増:', {
          delta: `${(memoryDelta / 1024 / 1024).toFixed(2)}MB`,
          domNodes: domDelta,
          timestamp: latest.timestamp
        });
      }
    }
  }
}

プロダクション環境でのデバッグ

// 1. プロダクション向けメモリモニタリング
class ProductionMemoryMonitor {
  constructor(config = {}) {
    this.endpoint = config.endpoint || '/api/metrics';
    this.sampleRate = config.sampleRate || 0.1; // 10%のユーザーでサンプリング
    this.batchSize = config.batchSize || 10;
    this.metrics = [];
  }
  
  shouldSample() {
    return Math.random() < this.sampleRate;
  }
  
  collectMetrics() {
    if (!this.shouldSample()) return;
    
    const metrics = {
      timestamp: Date.now(),
      url: window.location.href,
      userAgent: navigator.userAgent,
      memory: this.getMemoryMetrics(),
      performance: this.getPerformanceMetrics(),
      errors: this.getErrorMetrics()
    };
    
    this.metrics.push(metrics);
    
    if (this.metrics.length >= this.batchSize) {
      this.sendMetrics();
    }
  }
  
  getMemoryMetrics() {
    if (!performance.memory) return null;
    
    return {
      used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024),
      total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024),
      limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024)
    };
  }
  
  getPerformanceMetrics() {
    const navigation = performance.getEntriesByType('navigation')[0];
    
    return {
      loadTime: navigation ? navigation.loadEventEnd - navigation.fetchStart : null,
      domInteractive: navigation ? navigation.domInteractive : null,
      resourceCount: performance.getEntriesByType('resource').length
    };
  }
  
  getErrorMetrics() {
    // エラー数の追跡(グローバルエラーハンドラと連携)
    return {
      errorCount: window.__errorCount || 0,
      lastError: window.__lastError || null
    };
  }
  
  async sendMetrics() {
    if (this.metrics.length === 0) return;
    
    const batch = this.metrics.splice(0, this.batchSize);
    
    try {
      await fetch(this.endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ metrics: batch })
      });
    } catch (error) {
      console.error('Failed to send metrics:', error);
      // リトライのためにメトリクスを戻す
      this.metrics.unshift(...batch);
    }
  }
}

// 2. メモリリークのリアルタイム通知
class MemoryLeakAlert {
  constructor() {
    this.baseline = null;
    this.threshold = 100 * 1024 * 1024; // 100MB
    this.checkInterval = 300000; // 5分
  }
  
  start() {
    // ベースラインの設定
    setTimeout(() => {
      this.baseline = performance.memory?.usedJSHeapSize || 0;
      this.scheduleCheck();
    }, 10000); // 10秒後にベースラインを設定
  }
  
  scheduleCheck() {
    setInterval(() => {
      this.checkMemoryUsage();
    }, this.checkInterval);
  }
  
  checkMemoryUsage() {
    if (!performance.memory || !this.baseline) return;
    
    const current = performance.memory.usedJSHeapSize;
    const increase = current - this.baseline;
    
    if (increase > this.threshold) {
      this.sendAlert({
        baseline: this.baseline,
        current: current,
        increase: increase,
        percentage: ((increase / this.baseline) * 100).toFixed(2)
      });
    }
  }
  
  sendAlert(data) {
    // サーバーへの通知
    fetch('/api/alerts/memory-leak', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        ...data,
        userAgent: navigator.userAgent,
        url: window.location.href,
        timestamp: new Date().toISOString()
      })
    }).catch(console.error);
    
    // コンソールへの警告
    console.error('メモリリークの可能性:', data);
  }
}

// グローバルエラーハンドラの設定
window.__errorCount = 0;
window.addEventListener('error', (event) => {
  window.__errorCount++;
  window.__lastError = {
    message: event.message,
    source: event.filename,
    line: event.lineno,
    column: event.colno,
    timestamp: Date.now()
  };
});

パフォーマンス最適化のテクニック

メモリ最適化戦略の比較

メモリ最適化技術の比較
最適化技術 実装方法 メモリ削減効果 適用シーン
オブジェクトプーリング オブジェクトの再利用 70-90%削減 ゲーム、アニメーション
WeakMap/WeakSet 弱い参照の使用 50-70%削減 キャッシュ、メタデータ
TypedArray 固定サイズ配列 60-80%削減 数値データ処理
遅延読み込み 必要時のみロード 40-60%削減 SPA、大規模アプリ

実践的な最適化例

// メモリリークが発生しやすいコード
class DataManager {
  constructor() {
    this.cache = {}; // 強い参照
    this.handlers = [];
  }
  
  loadData(id) {
    // 毎回新しいオブジェクトを作成
    const data = {
      id: id,
      timestamp: new Date(),
      values: new Array(10000).fill(0),
      process: function() {
        // クロージャーが大量のデータを保持
        return this.values.map(v => v * 2);
      }
    };
    
    this.cache[id] = data;
    
    // イベントリスナーの蓄積
    document.addEventListener('click', () => {
      console.log(data.id);
    });
    
    return data;
  }
  
  clearCache() {
    // 不完全なクリーンアップ
    this.cache = {};
    // handlersはクリアされない
  }
}
// メモリ効率的なコード
class OptimizedDataManager {
  constructor() {
    this.cache = new WeakMap(); // 弱い参照
    this.handlers = new WeakMap();
    this.dataPool = []; // オブジェクトプール
  }
  
  loadData(id) {
    // オブジェクトプールから取得
    const data = this.dataPool.pop() || this.createDataObject();
    
    // データの初期化
    data.id = id;
    data.timestamp = Date.now(); // Dateオブジェクトを避ける
    
    // TypedArrayの使用
    if (!data.values) {
      data.values = new Float32Array(10000);
    }
    
    // キーオブジェクトを作成
    const key = { id };
    this.cache.set(key, data);
    
    // イベントリスナーの管理
    const handler = this.createHandler(data.id);
    this.handlers.set(key, handler);
    document.addEventListener('click', handler);
    
    return { key, getData: () => this.cache.get(key) };
  }
  
  createDataObject() {
    return {
      id: null,
      timestamp: null,
      values: null,
      // メソッドをプロトタイプに移動
    };
  }
  
  createHandler(id) {
    // クロージャーを最小化
    return function handler(event) {
      console.log(id);
    };
  }
  
  releaseData(key) {
    const data = this.cache.get(key);
    const handler = this.handlers.get(key);
    
    if (handler) {
      document.removeEventListener('click', handler);
      this.handlers.delete(key);
    }
    
    if (data) {
      // オブジェクトをプールに戻す
      data.id = null;
      data.timestamp = null;
      data.values.fill(0);
      
      if (this.dataPool.length < 100) {
        this.dataPool.push(data);
      }
      
      this.cache.delete(key);
    }
  }
  
  // 完全なクリーンアップ
  destroy() {
    // WeakMapは自動的にクリーンアップされる
    this.cache = new WeakMap();
    this.handlers = new WeakMap();
    this.dataPool = [];
  }
}

// 使用例
const manager = new OptimizedDataManager();
const dataRef = manager.loadData('user-123');

// 必要なくなったら明示的に解放
manager.releaseData(dataRef.key);
最適化前のコード
// メモリリークが発生しやすいコード
class DataManager {
  constructor() {
    this.cache = {}; // 強い参照
    this.handlers = [];
  }
  
  loadData(id) {
    // 毎回新しいオブジェクトを作成
    const data = {
      id: id,
      timestamp: new Date(),
      values: new Array(10000).fill(0),
      process: function() {
        // クロージャーが大量のデータを保持
        return this.values.map(v => v * 2);
      }
    };
    
    this.cache[id] = data;
    
    // イベントリスナーの蓄積
    document.addEventListener('click', () => {
      console.log(data.id);
    });
    
    return data;
  }
  
  clearCache() {
    // 不完全なクリーンアップ
    this.cache = {};
    // handlersはクリアされない
  }
}
最適化後のコード
// メモリ効率的なコード
class OptimizedDataManager {
  constructor() {
    this.cache = new WeakMap(); // 弱い参照
    this.handlers = new WeakMap();
    this.dataPool = []; // オブジェクトプール
  }
  
  loadData(id) {
    // オブジェクトプールから取得
    const data = this.dataPool.pop() || this.createDataObject();
    
    // データの初期化
    data.id = id;
    data.timestamp = Date.now(); // Dateオブジェクトを避ける
    
    // TypedArrayの使用
    if (!data.values) {
      data.values = new Float32Array(10000);
    }
    
    // キーオブジェクトを作成
    const key = { id };
    this.cache.set(key, data);
    
    // イベントリスナーの管理
    const handler = this.createHandler(data.id);
    this.handlers.set(key, handler);
    document.addEventListener('click', handler);
    
    return { key, getData: () => this.cache.get(key) };
  }
  
  createDataObject() {
    return {
      id: null,
      timestamp: null,
      values: null,
      // メソッドをプロトタイプに移動
    };
  }
  
  createHandler(id) {
    // クロージャーを最小化
    return function handler(event) {
      console.log(id);
    };
  }
  
  releaseData(key) {
    const data = this.cache.get(key);
    const handler = this.handlers.get(key);
    
    if (handler) {
      document.removeEventListener('click', handler);
      this.handlers.delete(key);
    }
    
    if (data) {
      // オブジェクトをプールに戻す
      data.id = null;
      data.timestamp = null;
      data.values.fill(0);
      
      if (this.dataPool.length < 100) {
        this.dataPool.push(data);
      }
      
      this.cache.delete(key);
    }
  }
  
  // 完全なクリーンアップ
  destroy() {
    // WeakMapは自動的にクリーンアップされる
    this.cache = new WeakMap();
    this.handlers = new WeakMap();
    this.dataPool = [];
  }
}

// 使用例
const manager = new OptimizedDataManager();
const dataRef = manager.loadData('user-123');

// 必要なくなったら明示的に解放
manager.releaseData(dataRef.key);

高度な活用方法

1. メモリ効率的な仮想スクロール

// 大量のデータを効率的に表示
class VirtualScroller {
  constructor(container, options = {}) {
    this.container = container;
    this.itemHeight = options.itemHeight || 50;
    this.buffer = options.buffer || 5;
    this.items = [];
    this.visibleRange = { start: 0, end: 0 };
    this.pool = new Map(); // DOM要素のプール
    
    this.setupScrollListener();
  }
  
  setItems(items) {
    this.items = items;
    this.updateView();
  }
  
  setupScrollListener() {
    let ticking = false;
    
    this.container.addEventListener('scroll', () => {
      if (!ticking) {
        requestAnimationFrame(() => {
          this.updateView();
          ticking = false;
        });
        ticking = true;
      }
    });
  }
  
  updateView() {
    const scrollTop = this.container.scrollTop;
    const containerHeight = this.container.clientHeight;
    
    // 可視範囲の計算
    const startIndex = Math.floor(scrollTop / this.itemHeight) - this.buffer;
    const endIndex = Math.ceil((scrollTop + containerHeight) / this.itemHeight) + this.buffer;
    
    this.visibleRange = {
      start: Math.max(0, startIndex),
      end: Math.min(this.items.length - 1, endIndex)
    };
    
    this.renderVisibleItems();
  }
  
  renderVisibleItems() {
    const fragment = document.createDocumentFragment();
    const usedElements = new Set();
    
    // スペーサー要素(上部)
    const spacerTop = document.createElement('div');
    spacerTop.style.height = `${this.visibleRange.start * this.itemHeight}px`;
    fragment.appendChild(spacerTop);
    
    // 可視アイテムのレンダリング
    for (let i = this.visibleRange.start; i <= this.visibleRange.end; i++) {
      const item = this.items[i];
      if (!item) continue;
      
      let element = this.getPooledElement(i);
      if (!element) {
        element = this.createElement(item, i);
      }
      
      this.updateElement(element, item, i);
      fragment.appendChild(element);
      usedElements.add(element);
    }
    
    // スペーサー要素(下部)
    const spacerBottom = document.createElement('div');
    const remainingItems = this.items.length - this.visibleRange.end - 1;
    spacerBottom.style.height = `${remainingItems * this.itemHeight}px`;
    fragment.appendChild(spacerBottom);
    
    // 未使用要素をプールに戻す
    this.pool.forEach((element, index) => {
      if (!usedElements.has(element)) {
        this.pool.delete(index);
      }
    });
    
    // DOMの更新
    this.container.innerHTML = '';
    this.container.appendChild(fragment);
  }
  
  getPooledElement(index) {
    return this.pool.get(index);
  }
  
  createElement(item, index) {
    const element = document.createElement('div');
    element.className = 'virtual-item';
    element.style.height = `${this.itemHeight}px`;
    element.style.position = 'absolute';
    element.style.top = `${index * this.itemHeight}px`;
    element.style.width = '100%';
    
    this.pool.set(index, element);
    return element;
  }
  
  updateElement(element, item, index) {
    element.textContent = item.text || `Item ${index}`;
    element.style.top = `${index * this.itemHeight}px`;
  }
}

2. SharedArrayBufferを使った高性能処理

// ワーカー間での効率的なデータ共有
class SharedMemoryProcessor {
  constructor(workerCount = navigator.hardwareConcurrency || 4) {
    this.workerCount = workerCount;
    this.workers = [];
    this.sharedBuffer = null;
    this.sharedArray = null;
  }
  
  async initialize(dataSize) {
    // SharedArrayBufferの作成
    this.sharedBuffer = new SharedArrayBuffer(dataSize * 4); // Float32
    this.sharedArray = new Float32Array(this.sharedBuffer);
    
    // ワーカーの初期化
    for (let i = 0; i < this.workerCount; i++) {
      const worker = new Worker('processor-worker.js');
      
      // SharedArrayBufferをワーカーに送信
      worker.postMessage({
        type: 'init',
        sharedBuffer: this.sharedBuffer,
        workerId: i,
        workerCount: this.workerCount
      });
      
      this.workers.push(worker);
    }
  }
  
  async processData(operation) {
    const promises = this.workers.map((worker, index) => {
      return new Promise((resolve) => {
        worker.onmessage = (e) => {
          if (e.data.type === 'result') {
            resolve(e.data.result);
          }
        };
        
        worker.postMessage({
          type: 'process',
          operation: operation
        });
      });
    });
    
    const results = await Promise.all(promises);
    return this.combineResults(results);
  }
  
  combineResults(results) {
    // 各ワーカーの結果を統合
    return results.reduce((acc, result) => {
      return acc + result;
    }, 0);
  }
  
  updateData(index, value) {
    // Atomicsを使った安全な更新
    Atomics.store(this.sharedArray, index, value);
  }
  
  terminate() {
    this.workers.forEach(worker => worker.terminate());
    this.workers = [];
    this.sharedBuffer = null;
    this.sharedArray = null;
  }
}

// processor-worker.js
let sharedArray;
let workerId;
let workerCount;

onmessage = function(e) {
  const { type } = e.data;
  
  switch (type) {
    case 'init':
      sharedArray = new Float32Array(e.data.sharedBuffer);
      workerId = e.data.workerId;
      workerCount = e.data.workerCount;
      break;
      
    case 'process':
      const result = processChunk(e.data.operation);
      postMessage({ type: 'result', result });
      break;
  }
};

function processChunk(operation) {
  const chunkSize = Math.ceil(sharedArray.length / workerCount);
  const start = workerId * chunkSize;
  const end = Math.min(start + chunkSize, sharedArray.length);
  
  let result = 0;
  
  for (let i = start; i < end; i++) {
    const value = Atomics.load(sharedArray, i);
    
    switch (operation) {
      case 'sum':
        result += value;
        break;
      case 'average':
        result += value / (end - start);
        break;
      case 'max':
        result = Math.max(result, value);
        break;
    }
  }
  
  return result;
}

3. メモリリークの自動修復システム

// メモリリークを自動的に検出して修復
class MemoryLeakRepairSystem {
  constructor() {
    this.leakPatterns = new Map();
    this.repairStrategies = new Map();
    this.monitoringInterval = 30000; // 30秒
    
    this.initializePatterns();
    this.startMonitoring();
  }
  
  initializePatterns() {
    // リークパターンの定義
    this.leakPatterns.set('event-listeners', {
      detect: () => this.detectEventListenerLeaks(),
      repair: () => this.repairEventListenerLeaks()
    });
    
    this.leakPatterns.set('dom-references', {
      detect: () => this.detectDOMReferenceLeaks(),
      repair: () => this.repairDOMReferenceLeaks()
    });
    
    this.leakPatterns.set('timers', {
      detect: () => this.detectTimerLeaks(),
      repair: () => this.repairTimerLeaks()
    });
  }
  
  startMonitoring() {
    setInterval(() => {
      this.performLeakDetection();
    }, this.monitoringInterval);
  }
  
  async performLeakDetection() {
    const detectedLeaks = [];
    
    for (const [name, pattern] of this.leakPatterns) {
      const leakInfo = await pattern.detect();
      
      if (leakInfo.hasLeak) {
        detectedLeaks.push({
          type: name,
          severity: leakInfo.severity,
          details: leakInfo.details
        });
        
        // 自動修復を試みる
        if (leakInfo.severity === 'high') {
          await pattern.repair();
        }
      }
    }
    
    if (detectedLeaks.length > 0) {
      this.reportLeaks(detectedLeaks);
    }
  }
  
  detectEventListenerLeaks() {
    const elements = document.querySelectorAll('*');
    let totalListeners = 0;
    const suspiciousElements = [];
    
    elements.forEach(element => {
      // Chrome DevTools APIを使用(利用可能な場合)
      if (typeof getEventListeners !== 'undefined') {
        const listeners = getEventListeners(element);
        const listenerCount = Object.values(listeners).flat().length;
        
        totalListeners += listenerCount;
        
        if (listenerCount > 10) {
          suspiciousElements.push({
            element,
            count: listenerCount
          });
        }
      }
    });
    
    return {
      hasLeak: totalListeners > 1000 || suspiciousElements.length > 0,
      severity: totalListeners > 5000 ? 'high' : 'medium',
      details: {
        totalListeners,
        suspiciousElements
      }
    };
  }
  
  repairEventListenerLeaks() {
    // グローバルなイベントリスナーマネージャーを作成
    if (!window.__eventManager) {
      window.__eventManager = new WeakMap();
    }
    
    // 既存のaddEventListenerをラップ
    const originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      // リスナーを追跡
      if (!window.__eventManager.has(this)) {
        window.__eventManager.set(this, new Map());
      }
      
      const listeners = window.__eventManager.get(this);
      if (!listeners.has(type)) {
        listeners.set(type, new Set());
      }
      
      listeners.get(type).add(listener);
      
      // オリジナルのメソッドを呼び出し
      return originalAddEventListener.call(this, type, listener, options);
    };
  }
  
  detectDOMReferenceLeaks() {
    // 切り離されたDOMノードの検出
    const detachedNodes = [];
    const allNodes = document.querySelectorAll('*');
    
    allNodes.forEach(node => {
      if (!document.body.contains(node) && node.nodeType === 1) {
        detachedNodes.push(node);
      }
    });
    
    return {
      hasLeak: detachedNodes.length > 100,
      severity: detachedNodes.length > 500 ? 'high' : 'medium',
      details: {
        detachedNodesCount: detachedNodes.length
      }
    };
  }
  
  repairDOMReferenceLeaks() {
    // DOMノードの弱い参照マップを作成
    if (!window.__domReferences) {
      window.__domReferences = new WeakMap();
    }
    
    // MutationObserverでDOMの変更を監視
    const observer = new MutationObserver((mutations) => {
      mutations.forEach(mutation => {
        mutation.removedNodes.forEach(node => {
          // 削除されたノードのクリーンアップ
          if (window.__eventManager && window.__eventManager.has(node)) {
            const listeners = window.__eventManager.get(node);
            listeners.forEach((listenerSet, type) => {
              listenerSet.forEach(listener => {
                node.removeEventListener(type, listener);
              });
            });
            window.__eventManager.delete(node);
          }
        });
      });
    });
    
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  }
  
  detectTimerLeaks() {
    // アクティブなタイマーの数を推定
    // 正確な数は取得できないため、パフォーマンスから推測
    const startTime = performance.now();
    let timerCount = 0;
    
    // ダミータイマーを設定して遅延を測定
    const testInterval = setInterval(() => {
      timerCount++;
    }, 1);
    
    setTimeout(() => {
      clearInterval(testInterval);
      const elapsed = performance.now() - startTime;
      const expectedCount = elapsed;
      const overhead = timerCount / expectedCount;
      
      // オーバーヘッドが大きい場合はタイマーが多い
      return {
        hasLeak: overhead < 0.8,
        severity: overhead < 0.5 ? 'high' : 'medium',
        details: {
          estimatedTimers: Math.round(1 / overhead)
        }
      };
    }, 100);
  }
  
  repairTimerLeaks() {
    // タイマーの管理システムを実装
    const originalSetTimeout = window.setTimeout;
    const originalSetInterval = window.setInterval;
    const activeTimers = new Set();
    
    window.setTimeout = function(...args) {
      const timerId = originalSetTimeout.apply(window, args);
      activeTimers.add(timerId);
      return timerId;
    };
    
    window.setInterval = function(...args) {
      const timerId = originalSetInterval.apply(window, args);
      activeTimers.add(timerId);
      return timerId;
    };
    
    // クリア時に追跡から削除
    const originalClearTimeout = window.clearTimeout;
    const originalClearInterval = window.clearInterval;
    
    window.clearTimeout = function(timerId) {
      activeTimers.delete(timerId);
      return originalClearTimeout.call(window, timerId);
    };
    
    window.clearInterval = function(timerId) {
      activeTimers.delete(timerId);
      return originalClearInterval.call(window, timerId);
    };
    
    // 定期的に未使用タイマーをクリア
    setInterval(() => {
      if (activeTimers.size > 100) {
        console.warn(`Active timers: ${activeTimers.size}`);
      }
    }, 60000);
  }
  
  reportLeaks(leaks) {
    console.group('メモリリーク検出レポート');
    leaks.forEach(leak => {
      console.warn(`${leak.type}: ${leak.severity}`, leak.details);
    });
    console.groupEnd();
    
    // サーバーへのレポート送信
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/memory-leaks', JSON.stringify(leaks));
    }
  }
}

// 自動修復システムの起動
const repairSystem = new MemoryLeakRepairSystem();

よくある質問と回答

Q: メモリリークが疑われる場合の初期対応は?

A: メモリリークが疑われる場合の段階的対応方法:

  1. パフォーマンスモニターで確認

    // Chrome DevToolsでの簡易チェック
    console.log('Memory:', performance.memory);
  2. ヒープスナップショットの取得

    • DevTools > Memory > Take Heap Snapshot
    • 時間を置いて複数回取得
    • Comparison ビューで差分を確認
  3. 一般的な原因のチェック

    • イベントリスナーの解除忘れ
    • タイマーのクリア忘れ
    • DOM 要素への参照保持
    • 大きなクロージャー
  4. Allocation Timelineでの追跡

    • リアルタイムでメモリ割り当てを監視
    • スパイクの原因を特定

Q: WeakMapとMapの使い分けは?

A: 以下の基準で使い分けます:

WeakMapを使うべき場合:

  • キーがオブジェクト
  • キーの生存期間が不明確
  • メタデータの保存
  • DOM 要素との関連付け

Mapを使うべき場合:

  • キーがプリミティブ値
  • 反復処理が必要
  • サイズの取得が必要
  • 明示的な削除管理
// WeakMapの例
const cache = new WeakMap();
function getComputedData(obj) {
  if (cache.has(obj)) return cache.get(obj);
  const data = expensiveComputation(obj);
  cache.set(obj, data);
  return data;
}

// Mapの例
const userSessions = new Map();
userSessions.set(userId, sessionData);
// 必要に応じて明示的に削除
userSessions.delete(userId);

Q: プロダクションでのメモリ監視はどうする?

A: プロダクション環境でのメモリ監視のベストプラクティス:

  1. サンプリング戦略

    • 全ユーザーの 5-10%でモニタリング
    • パフォーマンスへの影響を最小化
  2. メトリクスの収集

    • performance.memory API の活用
    • カスタムメトリクスの定義
    • バッチ送信でネットワーク負荷軽減
  3. アラートの設定

    • しきい値超過時の通知
    • 急激な増加パターンの検出
  4. 分析と対応

    • ダッシュボードでの可視化
    • トレンド分析
    • インシデント対応フロー

まとめ

JavaScriptのメモリ管理は、高品質な Web アプリケーション開発において不可欠な知識です。本記事で紹介した技術を活用することで、メモリ効率の良いアプリケーションを構築できます。重要なポイントは:

  • ガベージコレクションの仕組みを理解し、それに適したコードを書く
  • Chrome DevTools を使用して定期的にメモリプロファイリングを実施
  • よくあるメモリリークパターンを避け、適切なクリーンアップを実装
  • プロダクション環境でのメモリ監視を忘れずに行う

これらの実践により、ユーザーに快適な体験を提供できる Web アプリケーションを開発できるでしょう。

Rinaのプロフィール画像

Rina

Daily Hack 編集長

フルスタックエンジニアとして10年以上の経験を持つ。 大手IT企業やスタートアップでの開発経験を活かし、 実践的で即効性のある技術情報を日々発信中。 特にWeb開発、クラウド技術、AI活用に精通。

この記事は役に立ちましたか?

あなたのフィードバックが記事の改善に役立ちます

この記事は役に立ちましたか?

Daily Hackでは、開発者の皆様に役立つ情報を毎日発信しています。