0Pricing
Node.js Backend Development Bootcamp · レッスン

ヒープスナップショットの取得と比較

インスペクターでヒープスナップショットの差分を比較し、保持されているオブジェクトとリークの原因を見つけます。

「ヒープスナップショットの取得と比較」はCoddyKit上の無料Node.js Backend Development Bootcampレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはNode.js Backend Development Bootcamp学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Node.js Backend Development Bootcampコースには全4レッスンが含まれています。

このレッスンの一部はまだ翻訳されておらず、英語で表示されています。

Why Heap Snapshots Matter

A heap snapshot is a complete dump of every JavaScript object alive in V8 at the moment you capture it. For a Node.js backend, it is the single most precise tool for answering one question: what is still being retained, and why?

  • A leaking process keeps allocating objects that never get garbage-collected because something is still holding a reference.
  • One snapshot tells you what exists now; comparing two snapshots over time tells you what is growing — which is the actual leak signal.

In this lesson you will capture snapshots from a running Node service, load them into Chrome DevTools, diff them, and read the retainer chain back to the offending code.

Exposing the Inspector

To capture snapshots from a real service you first attach the V8 inspector. Start the process with --inspect so DevTools (or the inspector protocol) can connect.

  • node --inspect server.js opens the inspector on 127.0.0.1:9229.
  • Open chrome://inspect in Chrome, click inspect on your target, then go to the Memory tab.
  • Never bind the inspector to a public interface in production — it grants full code execution.

The snippet below is the exact CLI invocation you would script in your start command.

// package.json scripts
{
  "scripts": {
    "debug": "node --inspect=127.0.0.1:9229 server.js",
    "debug:brk": "node --inspect-brk server.js"
  }
}

Capturing a Snapshot Programmatically

You cannot always open DevTools against a production box. The built-in v8 module can write a .heapsnapshot file to disk on demand, which you later load into DevTools offline.

  • v8.writeHeapSnapshot(filename) serializes the entire heap synchronously.
  • Trigger it from a signal handler or an internal admin route so you can grab snapshots without restarting.

This program is fully standalone — it writes a snapshot and exits.

const v8 = require('v8');
const path = require('path');

function takeSnapshot(label) {
  const file = path.join(process.cwd(), `heap-${label}-${Date.now()}.heapsnapshot`);
  const written = v8.writeHeapSnapshot(file);
  console.log('Snapshot written to', written);
  return written;
}

takeSnapshot('baseline');

Triggering Snapshots on a Signal

A common production pattern is to listen for an OS signal and dump a snapshot without touching the running service. You send kill -USR2 <pid> and the process writes a file.

  • SIGUSR2 is conventionally free for app use (Nodemon uses it for restarts, so pick another if you run Nodemon).
  • Always force a GC consideration in mind: snapshots include unreachable objects until the next GC, so capture after the heap settles.
const v8 = require('v8');

process.on('SIGUSR2', () => {
  const file = `heap-${process.pid}-${Date.now()}.heapsnapshot`;
  v8.writeHeapSnapshot(file);
  console.log('Heap snapshot captured:', file);
});

console.log('Send: kill -USR2', process.pid);
setInterval(() => {}, 1 << 30);

The Three-Snapshot Technique

A single snapshot is noisy — it contains everything, including legitimate long-lived caches. The classic leak-hunting recipe is the three-snapshot technique:

  • Snapshot 1 — baseline, right after warm-up.
  • Exercise the suspect code path many times (e.g. hit an endpoint 1,000 times).
  • Snapshot 2 — after the workload.
  • Run the workload again, then take Snapshot 3.

Objects that appear in Snapshot 2 and persist into Snapshot 3 are the real leak — transient request objects will have been collected by then.

Forcing GC for Clean Snapshots

DevTools automatically runs a full GC before each snapshot, so what you see is truly reachable memory. When capturing programmatically you should do the same to avoid counting garbage that is about to disappear.

  • Run Node with --expose-gc to make global.gc() available.
  • Call global.gc() twice before snapshotting — the second pass cleans up objects freed during the first.
// run with: node --expose-gc snapshot.js
const v8 = require('v8');

function cleanSnapshot(label) {
  if (global.gc) {
    global.gc();
    global.gc();
  }
  return v8.writeHeapSnapshot(`heap-${label}.heapsnapshot`);
}

console.log(cleanSnapshot('after-gc'));

Reading the Summary View

Load a .heapsnapshot into DevTools Memory tab. The default Summary view groups objects by constructor.

  • Objects Count — how many live instances of that constructor exist.
  • Shallow Size — memory held by the object itself, excluding what it references.
  • Retained Size — memory that would be freed if this object were deleted, including everything only it keeps alive. This is the number that matters for leaks.

Sort by Retained Size descending to find the objects that dominate the heap.

Comparison View: Diffing Two Snapshots

The real power is the Comparison view. After loading two snapshots, switch the dropdown from Summary to Comparison and pick the baseline as the comparison base.

  • #New — objects allocated since the baseline.
  • #Deleted — objects collected since the baseline.
  • #Delta — net change. A constructor with a large positive delta that keeps growing across diffs is your leak.
  • Size Delta — net retained bytes added.

Focus on positive deltas whose count rises in lockstep with your workload iterations.

Building a Reproducible Leak

To practice the workflow, you need a deterministic leak. A module-level array (or Map) that you keep pushing into — and never clear — is the canonical example. Each request appends data that can never be collected.

  • The leaked array is reachable from the module scope, so V8 must keep every entry.
  • In DevTools this shows up as a growing (array) or your closure constructor with a rising delta.

This standalone program leaks on purpose and prints heap growth.

const leaked = [];

function handleRequest(i) {
  // Bug: we never remove old entries
  leaked.push({ id: i, payload: 'x'.repeat(1024), ts: Date.now() });
}

for (let i = 0; i < 5000; i++) handleRequest(i);

const mb = process.memoryUsage().heapUsed / 1024 / 1024;
console.log('Entries retained:', leaked.length);
console.log('Heap used (MB):', mb.toFixed(1));

Following the Retainer Chain

Once a suspicious constructor is found, click it to expand instances, then inspect the bottom Retainers pane. Retainers answer the only question that matters: who is holding this object alive?

  • The chain reads from the object up to a GC root (the global object, a module closure, a still-pending Promise, an active timer, etc.).
  • A yellow-highlighted node is reachable directly from a JS variable; red marks a detached DOM-style node (rare in Node).
  • Follow the chain until you recognize your variable — that is the line of code to fix.

Closures over large scopes, unbounded caches, and EventEmitter listeners that are never removed are the usual culprits.

const EventEmitter = require('events');
const bus = new EventEmitter();

// Leak: a new listener per call, never removed
function subscribe(userId) {
  const bigContext = { userId, cache: new Array(10000).fill(userId) };
  bus.on('tick', () => bigContext.cache[0]);
}

for (let i = 0; i < 200; i++) subscribe(i);
console.log('Listener count:', bus.listenerCount('tick'));

Confirming the Fix With a Final Diff

After patching the leak, prove it with the same three-snapshot ritual. Re-run the identical workload and diff baseline against the post-workload snapshot.

  • The previously growing constructor should now show a delta near zero — allocations and collections balance out.
  • Use a WeakMap or a bounded LRU cache so entries become eligible for GC when no longer referenced.
  • Automate a guardrail: assert that heapUsed after GC stays under a threshold across N iterations in a soak test.
const cache = new WeakMap();

function attach(req) {
  // Keyed by the request object; entry is collectable once req is gone
  cache.set(req, { processedAt: Date.now() });
}

let req1 = { id: 1 };
attach(req1);
console.log('Has entry:', cache.has(req1));
req1 = null; // now eligible for GC; WeakMap will not retain it
console.log('Reference dropped — entry can be collected');

Quick Check

You take three heap snapshots using the standard leak-hunting technique. Which objects are the strongest evidence of a real memory leak?

Recap

You now have a complete heap-snapshot leak-hunting workflow for Node.js backends:

  • Capture snapshots via --inspect + DevTools, v8.writeHeapSnapshot(), or a SIGUSR2 handler in production.
  • Force GC (--expose-gc, double global.gc()) so snapshots reflect only reachable memory.
  • Use the three-snapshot technique to separate real leaks from transient allocations.
  • In the Comparison view, hunt rising #Delta counts; sort by Retained Size.
  • Follow the retainer chain to the GC root to find the exact variable at fault — usually an unbounded cache, a lingering closure, or an unremoved listener.
  • Confirm the fix with another diff showing a flat delta; prefer WeakMap or bounded caches.

よくある質問

「ヒープスナップショットの取得と比較」レッスンは無料ですか?

はい。「ヒープスナップショットの取得と比較」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Node.js Backend Development Bootcampコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Node.js Backend Development Bootcampコースには全4レッスンが含まれています。

「ヒープスナップショットの取得と比較」で何を学びますか?

インスペクターでヒープスナップショットの差分を比較し、保持されているオブジェクトとリークの原因を見つけます。 ブラウザで直接実行するハンズオンコードでNode.js Backend Development Bootcampを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Node.js Backend Development Bootcampを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのNode.js Backend Development Bootcampは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。

「ヒープスナップショットの取得と比較」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このNode.js Backend Development Bootcampレッスンでコードを書いて実行できますか?

はい。すべてのNode.js Backend Development Bootcampレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. V8ヒープ、世代別GC、オブジェクトのライフサイクル
  2. ヒープスナップショットの取得と比較
  3. ホットパス向けCPUプロファイリングとフレームグラフ
  4. 一般的なリークパターンの検出と修正
← Node.js Backend Development Bootcampに戻る