| name | chrome-devtools-memory-leak |
| description | JavaScript/Node.js memory leak debugging via Chrome DevTools MCP. Heap snapshot workflow (baseline→target→final), memlab analysis, common leak patterns (detached DOM, closures, unbounded caches, event listeners). Sources: ChromeDevTools/chrome-devtools-mcp (Apache-2.0). |
/chrome-devtools-memory-leak
When to Use
- Page memory grows over time without releasing (OOM errors, tab crashes)
- Node.js server RSS grows unbounded under load
- Need to identify which objects are growing between interactions
- Investigating detached DOM nodes, retained closures, or unbounded caches
Do NOT use for
- CPU performance bottlenecks (use [[chrome-devtools-lcp-debug]])
- Network-related slowdowns
- Memory analysis on non-Chromium browsers
Three-snapshot workflow
Baseline snapshot → take before any interaction (clean initial state)
Target snapshot → take after N repetitions of the suspect interaction
Final snapshot → take after reverting actions (if memory not released → leak confirmed)
Rule: repeat the interaction 10× to amplify the leak signal before snapshotting.
await mcp.call("navigate_page", { url: "https://app.example.com/page" });
await mcp.call("wait_for", { selector: "[data-loaded]" });
await mcp.call("take_heapsnapshot", { filePath: "/tmp/baseline.heapsnapshot" });
for (let i = 0; i < 10; i++) {
await mcp.call("click", { uid: "open-modal-uid" });
await mcp.call("wait_for", { selector: ".modal" });
await mcp.call("click", { uid: "close-modal-uid" });
await mcp.call("wait_for", { networkIdle: true });
}
await mcp.call("take_heapsnapshot", { filePath: "/tmp/target.heapsnapshot" });
await mcp.call("navigate_page", { url: "https://app.example.com/page" });
await mcp.call("take_heapsnapshot", { filePath: "/tmp/final.heapsnapshot" });
Analyze with memlab (preferred — never read raw .heapsnapshot files)
npm install -g @memlab/cli
memlab analyze --baseline /tmp/baseline.heapsnapshot \
--target /tmp/target.heapsnapshot \
--final /tmp/final.heapsnapshot
Fallback: compare snapshots without memlab
await mcp.call("get_heapsnapshot_summary", {
snapshotFile: "/tmp/target.heapsnapshot"
});
await mcp.call("get_heapsnapshot_class_nodes", {
snapshotFile: "/tmp/target.heapsnapshot",
className: "HTMLDivElement",
limit: 10,
});
await mcp.call("get_heapsnapshot_retainers", {
snapshotFile: "/tmp/target.heapsnapshot",
nodeId: 12345,
});
Common leak patterns and fixes
const modal = document.createElement('div');
document.body.appendChild(modal);
document.body.removeChild(modal);
document.body.removeChild(modal);
modal = null;
function openModal() {
document.addEventListener('keydown', handleKeyDown);
}
function openModal() { document.addEventListener('keydown', handleKeyDown); }
function closeModal() { document.removeEventListener('keydown', handleKeyDown); }
() {
sum = largeArray.( a + b, );
.(sum);
}
() {
sum = largeArray.( a + b, );
largeArray = ;
.(sum);
}
cache = ();
() {
(!cache.(id)) cache.(id, (id));
cache.(id);
}
;
cache = ({ : , : * * });
Node.js server memory leak workflow
node --inspect app.js
curl -X POST http://localhost:9229/json/v1/profiler/takeHeapSnapshot \
-o /tmp/server-heap.heapsnapshot
memlab analyze --target /tmp/server-heap.heapsnapshot
setInterval(() => {
const { heapUsed, rss } = process.memoryUsage();
console.log(`heap: ${(heapUsed/1e6).toFixed(1)}MB rss: ${(rss/1e6).toFixed(1)}MB`);
}, 5000);
Anti-Fake-Pass Checklist
❌ Reading raw .heapsnapshot file directly → files are 100–500 MB; will exhaust token budget
❌ Single snapshot without baseline → no delta = no leak evidence
❌ Fewer than 10 repetitions → small leak signal is noise; amplify to 10x first
❌ Nulling detached DOM without confirming it's not an intentional cache → ask user first
❌ Fixing closure without profiling first → premature optimization; confirm with memlab trace
❌ Assuming OOM = memory leak → could be unbounded legitimate data growth; measure first