【実務・中級編】 スタックとヒープのメモリ管理概念 – JavaScript実践ガイド

メモリの「裏側」を知る者は、パフォーマンスを制す:スタックとヒープの深淵

フロントエンドの現場で「なぜかアプリが重い」「メモリリークでブラウザが固まる」という壁にぶつかったことはないだろうか?

多くのエンジニアは `const` や `let` を使い分け、オブジェクトを生成する。だが、その裏でJavaScriptエンジン(V8など)がメモリをどう確保し、どう解体しているのかを知っているかどうかで、書くコードの「格」が変わる。

今日は、中級から一歩先へ進む君たちのために、「スタック」と「ヒープ」というメモリの深淵について、泥臭い現実を交えて紐解いていこう。

—

1. スタックとヒープ:メモリの住所録

JavaScriptのメモリ管理は、大きく分けて「スタック領域」と「ヒープ領域」の2つの役割分担で成り立っている。

  • スタック領域 (Stack):

ここは「固定サイズ」の小さな部屋が並ぶマンションのようなものだ。プリミティブ型(`Number`, `String`, `Boolean`, `null`, `undefined`, `Symbol`, `BigInt`)がここに住む。
スタックはアクセスが爆速だ。しかし、サイズが限られており、データが不要になれば即座に破棄される「短命な場所」である。

  • ヒープ領域 (Heap):

ここは巨大で複雑な「倉庫」だ。オブジェクトや配列、関数のような「サイズが予測できない巨大なデータ」は、ここに投げ込まれる。
スタックにあるのは、ヒープ上の倉庫の「場所を示す鍵(参照)」だけだ。この「鍵」を持つことこそが、JavaScriptの参照渡しの正体だ。

—

2. 「なぜオブジェクトの比較はうまくいかないのか?」の正体

現場でよくあるバグに、`{ a: 1 } === { a: 1 }` が `false` になるという問題がある。なぜか?

スタックには「鍵(メモリアドレス)」しか入っていないからだ。たとえ中身が同じでも、ヒープ上の別の倉庫に作られたデータは、異なるアドレスを持つ。つまり、比較しているのは中身ではなく「倉庫の場所」なんだ。

// 実務でよくある比較の罠
const objA = { id: 1 };
const objB = { id: 1 };

// falseになる。なぜなら、メモリ上の異なる場所(ヒープ)を指しているから。
console.log(objA === objB);

// こちらはtrue。同じ場所を指す「鍵」をコピーしただけだから。
const objC = objA;
console.log(objA === objC);

—

3. ガベージコレクション(GC)と「孤児」の運命

メモリ管理で最も恐ろしいのは「メモリリーク」だ。JavaScriptエンジンは「ガベージコレクター(GC)」という掃除屋を常に巡回させている。

GCは「どこからも参照されていない(スタックからヒープへ辿り着けない)データ」を見つけては、ヒープから削除する。これを「マーク・アンド・スイープ」アルゴリズムと呼ぶ。

問題は、意図せず「不要なオブジェクトへの参照」をどこかに残してしまった時だ。これがメモリリークの温床になる。

現場での回避策:WeakMapの活用

DOM要素をキーに何かデータを保持したい時、通常の `Map` を使うと、DOMを削除しても `Map` が参照を持ち続け、GCが掃除できなくなる。ここで `WeakMap` の出番だ。

// メモリリークを防ぐための知恵
const cache = new WeakMap();

function attachData(element, data) {
// WeakMapはキーであるオブジェクトへの「弱い参照」しか持たない。
// elementが消えれば、GCが勝手にデータを掃除してくれる。
cache.set(element, data);
}

const button = document.querySelector(‘#my-btn’);
attachData(button, { clickCount: 0 });

// DOMから削除すれば、WeakMapのメモリも自動で解放される(安全!)
button.remove();

—

4. パフォーマンスを最適化するシニアの視点

最後に、実務で意識すべき「メモリ節約術」を伝授しよう。

1. 過度なオブジェクト生成を避ける:
ループ内で巨大なオブジェクトを次々と生成すると、ヒープの圧迫とGCの頻発を招き、UIのジャンク(カクつき)が発生する。可能なら変数を使い回すか、プリミティブを優先する。
2. クロージャの寿命に注意:
関数内関数(クロージャ)は、外側のスコープの変数を「ずっと保持」してしまう。巨大な配列などをクロージャ内に閉じ込めると、メモリが解放されないままになる。
3. 明示的な null 代入(現代では不要だが知識として):
昔のコードでよく見た `obj = null` は、もはや現代のエンジンではあまり意味をなさないが、「このオブジェクトはもう使わない」という意思表示として、巨大なデータ構造の破棄時にマークしておくのは悪い癖ではない。

—

まとめ:メモリを「意識」するということ

JavaScriptはメモリ管理を自動化してくれる便利な言語だが、それは「何も考えなくていい」という意味ではない。

スタック上の軽やかなデータと、ヒープ上の重厚なデータ。この二つの住み分けを意識できるようになると、コードを書くときに「今、このデータはどこにいて、いつ消えるべきか」が透けて見えるようになる。

それができれば、君はもう一段階上のエンジニアだ。さあ、ブラウザのデベロッパーツールを開いて、メモリタブを眺めてみよう。そこには、君のコードが呼吸している世界が広がっているはずだ。

コメント

タイトルとURLをコピーしました