概要
「遅くなった」「ブラウザが落ちる」「時間が経つとメモリ使用量が膨れ上がる」
こういった問題の多くは、**メモリの解放忘れ(リーク)**によって起こる。
JavaScriptでは手動でメモリ解放を行うことはできない。
代わりに、**ガベージコレクション(GC)**という仕組みが自動的に不要なメモリを解放してくれる。
だがこのGC、完全ではない。
正しく動作させるためには、“解放可能な状態”を自分で設計してやる必要がある。
本記事では、JavaScriptのメモリ管理の基本から、実務で発生しがちなメモリリークとその防止戦略までを解説する。
対象環境
JavaScript(ES5〜ES2023)
ブラウザ / Node.js 両対応
ガベージコレクション(GC)の概要
✅ 基本原則:参照されなくなったメモリは自動的に回収される
GCのアルゴリズム(主要ブラウザ実装):
-
Mark-and-Sweep(マーク&スイープ)
- ルート(グローバル変数・コールスタックなど)から到達可能なオブジェクトを「マーク」
- それ以外の「到達不能なオブジェクト」をメモリからスイープ(除去)
到達可能性の例
let obj = {
child: {
name: 'toto'
}
};
obj = null;
→ obj.child
も到達不能になる → GCによって回収される
なぜメモリリークが起きるのか?
GCは「参照がある限りは解放しない」ため、
意図しない参照が残っていると、使っていないのにメモリを保持し続けることになる。
よくあるメモリリークのパターンと対策
1. グローバル変数に値を残す
window.leak = createLargeObject(); // ❌ GCされない
→ ✅ 関数スコープ内で使い切る、グローバル汚染を避ける
2. クロージャ内に不要な値を保持
function leak() {
let large = new Array(1000000).fill('...');
return () => console.log(large.length);
}
const f = leak();
→ large
はずっと生き続ける
→ ✅ クロージャを使う場合は、本当に必要な変数だけを閉じ込める
3. イベントリスナーの解除忘れ
const el = document.getElementById('btn');
el.addEventListener('click', () => {
// リスナーがelを保持し続ける
});
→ ✅ removeEventListener
を明示的に呼ぶ、もしくは AbortController
を使う
4. タイマーやintervalが残り続ける
setInterval(() => {
// 無限ループで実行され続ける
}, 1000);
→ ✅ clearInterval()
をコンポーネント破棄時に行う(SPAで重要)
5. DOM参照を閉じ込める
const nodes = [];
document.querySelectorAll('*').forEach(node => nodes.push(node));
→ ✅ 必要なDOMだけをキャッシュし、不要になったら null に
ツールで可視化する:Chrome DevTools編
- DevTools → [Memory] タブを開く
- 「Take snapshot」でヒープスナップショットを撮影
- オブジェクトグループを比較し、
Detached DOM tree
やlistener
を確認 - メモリが減っていない=リークの兆候
WeakMap / WeakSet による安全な一時保持
const cache = new WeakMap();
function cacheData(obj, data) {
cache.set(obj, data);
}
→ ✅ GC対象となるオブジェクトをキーにすれば、参照が切れると自動解放
状態管理やフレームワークにおける注意
- React / Vue のコンポーネントでは、useEffectのクリーンアップや watchの解除が不可欠
- グローバルストアに巨大データを保持し続ける設計は危険
結語
JavaScriptのメモリは、自動管理されている。
だが「自動だから任せていい」わけではない。
任せられる状態を、意図して設計することが求められる。
- 使い終わった値は開放できる構造か
- イベントやクロージャに意図しない参照はないか
- フレームワークの裏で何が残っているのか
目に見えない世界を制御するには、見える構造をコードとして設計するしかない。
メモリ管理とは、パフォーマンスチューニングではない。
持続可能なアプリケーション設計の基礎である。