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の手順:
- Chrome DevToolsを開く(F12)
- Memoryタブを選択
- "Take heap snapshot"をクリック
- 問題のある操作を実行
- 再度snapshotを取得
- 比較モードで差分を確認
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アプリケーションを開発してください。
最短で課題解決する一冊
この記事の内容と高い親和性が確認できたベストマッチです。早めにチェックしておきましょう。
