
Memory Leak Debugging
Interpret memlab retainer traces and map common leak patterns (listeners, detached DOM, globals, closures) to concrete fixes in web apps.
Install
npx skills add https://github.com/chromedevtools/chrome-devtools-mcp --skill memory-leak-debuggingWhat is this skill?
- Maps memlab retainer traces to four documented leak categories
- Covers uncleared event listeners on window, document, and long-lived objects
- Flags detached DOM nodes with ask-user-first before treating as bugs
- Addresses unintentional globals and closure retention of large outer-scope objects
- Each pattern includes a concrete remediation direction
Adoption & trust: 664 installs on skills.sh; 43.1k GitHub stars; 3/3 security scanners passed (skills.sh audits); trending (+200% hot-view momentum).
Recommended Skills
Azure Diagnosticsmicrosoft/azure-skills
Diagnosemattpocock/skills
Systematic Debuggingobra/superpowers
Safe Debuglllllllama/rigorpilot-skills
Mastramastra-ai/skills
Insforge Debuginsforge/agent-skills
Journey fit
Common Questions / FAQ
Is Memory Leak Debugging safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.
SKILL.md
READMESKILL.md - Memory Leak Debugging
# Common Memory Leaks When analyzing a retainer trace from `memlab`, look for these common patterns in the codebase: ## 1. Uncleared Event Listeners Event listeners attached to global objects (like `window` or `document`) or long-living objects prevent garbage collection of the objects referenced in their callbacks. **Fix:** Always call `removeEventListener` when a component unmounts or the listener is no longer needed. ## 2. Detached DOM Nodes A DOM node is removed from the document tree but is still referenced by a JavaScript variable. While detachedness is a good signal for a memory leak, it's not always a bug. For example, websites sometimes intentionally cache detached navigation trees. **Fix:** Signal the detached nodes to the user first. **Ask the user first** before nulling the references or changing the code, as the detached nodes might be part of an intentional cache. If confirmed as a leak, ensure variables holding DOM references are set to `null` when the node is removed, or limit their scope. ## 3. Unintentional Global Variables Variables declared without `var`, `let`, or `const` (in non-strict mode) or explicitly attached to `window` remain in memory forever. **Fix:** Use strict mode, properly declare variables, and avoid global state. ## 4. Closures Closures can unintentionally keep references to large objects in their outer scope. **Fix:** Nullify large objects when they are no longer needed, or refactor the closure to not capture unnecessary variables. ## 5. Unbounded Caches or Arrays Data structures used for caching (like objects, Arrays, or Maps) that grow without limits. **Fix:** Implement caching limits, use LRU caches, or use `WeakMap`/`WeakSet` for data associated with object lifecycles. /** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import * as fs from 'node:fs'; function parseSnapshot(filePath) { console.log(`Loading ${filePath}...`); const data = JSON.parse(fs.readFileSync(filePath, 'utf8')); const strings = data.strings; const nodes = data.nodes; const nodeFields = data.snapshot.meta.node_fields; const nodeFieldCount = nodeFields.length; const typeOffset = nodeFields.indexOf('type'); const nameOffset = nodeFields.indexOf('name'); const sizeOffset = nodeFields.indexOf('self_size'); const nodeTypes = data.snapshot.meta.node_types[typeOffset]; const counts = {}; const sizes = {}; for (let i = 0; i < nodes.length; i += nodeFieldCount) { const typeIdx = nodes[i + typeOffset]; const typeName = nodeTypes[typeIdx]; const nameIdx = nodes[i + nameOffset]; const name = typeof nameIdx === 'number' ? strings[nameIdx] : nameIdx; const size = nodes[i + sizeOffset]; // Ignore native primitives/arrays that clutter the output unless specifically looking for them if ( typeName === 'string' || typeName === 'number' || typeName === 'array' ) { continue; } const key = `${typeName}::${name}`; counts[key] = (counts[key] || 0) + 1; sizes[key] = (sizes[key] || 0) + size; } return {counts, sizes}; } const [, , file1, file2] = process.argv; if (!file1 || !file2) { console.error( 'Usage: node compare_snapshots.js <baseline.heapsnapshot> <target.heapsnapshot>', ); process.exit(1); } try { const snap1 = parseSnapshot(file1); const snap2 = parseSnapshot(file2); const diffs = []; for (const key in snap2.counts) { const count1 = snap1.counts[key] || 0; const count2 = snap2.counts[key]; const size1 = snap1.sizes[key] || 0; const size2 = snap2.sizes[key]; if (count2 > count1) { diffs.push({ key, countDiff: count2 - count1, sizeDiff: size2 - size1, }); } } diffs.sort((a, b) => b.sizeDiff - a.sizeDiff); console.log('\n--- Top 10 growing objects by size ---'); diffs.slice(0, 10).forEach(d => { console.log(`${d.key}: +${d.countDiff} objects, +${d.sizeDiff} bytes`); }); // Lo