Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Expert skill for JavaScript memory leak detection using Facebook MemLab. Configure MemLab scenarios, execute memory leak detection runs, analyze heap snapshots, identify detached DOM elements, find event listener leaks, and integrate with CI pipelines.
.claude/skills/a5c-ai-memlab-analysis/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 84% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 152% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 142% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 106% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 76% | 0% |
You are memlab-analysis - a specialized skill for JavaScript memory leak detection using Facebook's MemLab framework. This skill provides expert capabilities for detecting, analyzing, and fixing memory leaks in web applications.
This skill enables AI-powered JavaScript memory analysis including:
npm install -g memlabWrite comprehensive MemLab test scenarios:
javascript// scenario.js - Basic memory leak detection scenario module.exports = { // Scenario metadata name: 'user-dashboard-leak-test', // Setup - navigate to starting page async setup(page) { await page.goto('https://app.example.com/'); await page.waitForSelector('.login-form'); }, // Action - perform the operation that may leak async action(page) { // Login await page.type('#email', 'test@example.com'); await page.type('#password', 'password123'); await page.click('#login-button'); await page.waitForSelector('.dashboard'); // Navigate to dashboard await page.click('[data-testid="analytics-tab"]'); await page.waitForSelector('.analytics-charts'); // Interact with charts (potential leak source) await page.click('[data-testid="chart-filter"]'); await page.waitForSelector('.chart-updated'); }, // Back - return to a clean state async back(page) { // Navigate away from the potentially leaking page await page.click('[data-testid="home-tab"]'); await page.waitForSelector('.dashboard-home'); }, // Optional: custom leak filter leakFilter(node, snapshot, leakedNodeIds) { // Ignore known non-leaks if (node.name === 'InternalCache') return false; if (node.retainedSize < 1024) return false; // Ignore small leaks return true; } };
Complex scenario configurations:
javascript// modal-leak-scenario.js - Test modal dialog memory leaks module.exports = { name: 'modal-dialog-leak', // Initial page state url: () => 'https://app.example.com/products', async setup(page) { await page.setViewport({ width: 1920, height: 1080 }); await page.evaluate(() => { window.memlab = { startTime: Date.now() }; }); }, async action(page) { // Open modal await page.click('[data-testid="add-product-btn"]'); await page.waitForSelector('.modal-overlay'); // Fill form await page.type('[name="productName"]', 'Test Product'); await page.type('[name="description"]', 'Test Description'); // Upload image (potential leak) const input = await page.$('[type="file"]'); await input.uploadFile('./test-image.png'); await page.waitForSelector('.image-preview'); // Close modal (should clean up) await page.click('.modal-close'); await page.waitForSelector('.modal-overlay', { hidden: true }); }, async back(page) { // Force garbage collection opportunity await page.evaluate(() => { window.dispatchEvent(new Event('beforeunload')); }); await page.goto('https://app.example.com/'); }, // Repeat the action multiple times to amplify leaks repeat: () => 3, // Custom leak detection leakFilter(node, snapshot, leakedNodeIds) { // Focus on specific leak patterns const suspectTypes = [ 'HTMLDivElement', 'HTMLImageElement', 'EventListener', 'Closure' ]; return suspectTypes.includes(node.type); } };
Test memory leaks during route changes:
javascript// route-navigation-scenario.js module.exports = { name: 'spa-route-navigation', async setup(page) { await page.goto('https://app.example.com/'); await page.waitForNetworkIdle(); }, async action(page) { // Navigate through multiple routes const routes = [ '/dashboard', '/products', '/orders', '/settings', '/analytics' ]; for (const route of routes) { await page.click(`a[href="${route}"]`); await page.waitForNetworkIdle(); await page.waitForTimeout(500); } }, async back(page) { await page.click('a[href="/"]'); await page.waitForNetworkIdle(); } };
Detect event listener accumulation:
javascript// event-listener-scenario.js module.exports = { name: 'event-listener-leak', async setup(page) { await page.goto('https://app.example.com/'); // Inject event listener counter await page.evaluate(() => { const originalAddEventListener = EventTarget.prototype.addEventListener; const originalRemoveEventListener = EventTarget.prototype.removeEventListener; window.__eventListenerCount = 0; window.__eventListeners = new Map(); EventTarget.prototype.addEventListener = function(type, listener, options) { window.__eventListenerCount++; const key = `${this.constructor.name}:${type}`; window.__eventListeners.set(key, (window.__eventListeners.get(key) || 0) + 1); return originalAddEventListener.call(this, type, listener, options); }; EventTarget.prototype.removeEventListener = function(type, listener, options) { window.__eventListenerCount--; const key = `${this.constructor.name}:${type}`; window.__eventListeners.set(key, (window.__eventListeners.get(key) || 0) - 1); return originalRemoveEventListener.call(this, type, listener, options); }; }); }, async action(page) { // Perform actions that add event listeners await page.click('[data-testid="open-sidebar"]'); await page.waitForSelector('.sidebar'); await page.click('[data-testid="close-sidebar"]'); await page.waitForSelector('.sidebar', { hidden: true }); }, async back(page) { // Check listener count const stats = await page.evaluate(() => ({ count: window.__eventListenerCount, listeners: Object.fromEntries(window.__eventListeners) })); console.log('Event listener stats:', stats); await page.goto('https://app.example.com/'); } };
Execute MemLab commands:
bash# Basic leak detection memlab run --scenario scenario.js # Run with increased iterations memlab run --scenario scenario.js --work-dir ./memlab-results # Run specific phases memlab snapshot --scenario scenario.js memlab find-leaks --work-dir ./memlab-results # Analyze existing heap snapshots memlab analyze ./memlab-results # Generate detailed report memlab report --work-dir ./memlab-results --output-dir ./reports # Run in headless mode memlab run --scenario scenario.js --headless # Custom Chromium path memlab run --scenario scenario.js --chromium-binary /path/to/chrome
Analyze heap snapshots for memory issues:
javascript// heap-analysis.js - Custom heap analysis const { takeNodeMinimalHeap, findLeaks } = require('@memlab/api'); async function analyzeHeap() { // Take heap snapshot const heap = await takeNodeMinimalHeap(); // Find objects by type const detachedDOMNodes = heap.nodes.filter(node => node.name.startsWith('Detached ') && node.type === 'native' ); // Find large retained objects const largeObjects = heap.nodes .filter(node => node.retainedSize > 1024 * 1024) // > 1MB .sort((a, b) => b.retainedSize - a.retainedSize) .slice(0, 10); // Find specific patterns const closureLeaks = heap.nodes.filter(node => node.type === 'closure' && node.retainedSize > 10240 ); console.log('Analysis Results:'); console.log('Detached DOM nodes:', detachedDOMNodes.length); console.log('Large objects:', largeObjects.map(n => ({ name: n.name, type: n.type, size: `${(n.retainedSize / 1024 / 1024).toFixed(2)} MB` }))); console.log('Potential closure leaks:', closureLeaks.length); }
Integrate MemLab into CI pipelines:
yaml# .github/workflows/memory-check.yml name: Memory Leak Check on: pull_request: branches: [main] jobs: memlab: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' - name: Install dependencies run: npm ci - name: Build application run: npm run build - name: Start application run: npm run start & env: PORT: 3000 - name: Wait for application run: npx wait-on http://localhost:3000 - name: Install MemLab run: npm install -g memlab - name: Run memory leak tests run: | memlab run --scenario ./tests/memlab/dashboard-scenario.js \ --work-dir ./memlab-results \ --headless - name: Check for leaks run: | LEAK_COUNT=$(memlab find-leaks --work-dir ./memlab-results --json | jq '.length') if [ "$LEAK_COUNT" -gt 0 ]; then echo "::error::Found $LEAK_COUNT memory leaks" memlab report --work-dir ./memlab-results exit 1 fi - name: Upload artifacts if: failure() uses: actions/upload-artifact@v4 with: name: memlab-results path: ./memlab-results
Identify common JavaScript memory leak patterns:
javascript// leak-patterns.js - Detect common leak patterns module.exports = { // Detached DOM elements detectDetachedDOM(node) { return node.name.startsWith('Detached ') && ['HTMLDivElement', 'HTMLSpanElement', 'HTMLImageElement'] .some(type => node.name.includes(type)); }, // Event listener leaks detectEventListenerLeak(node) { return node.type === 'object' && node.name === 'EventListener' && node.retainedSize > 1024; }, // Closure leaks (holding references) detectClosureLeak(node) { return node.type === 'closure' && node.retainedSize > 10240 && node.edges.some(edge => edge.name === 'context'); }, // Timer leaks (setInterval not cleared) detectTimerLeak(node) { return node.name === 'Timeout' || node.name === 'Interval'; }, // Promise chain leaks detectPromiseLeak(node) { return node.name === 'Promise' && node.edges.some(edge => edge.name === 'reactions' && edge.to.retainedSize > 0 ); }, // Component state retention detectComponentLeak(node) { const componentPatterns = [ 'FiberNode', // React 'ComponentPublicInstance', // Vue 'ViewRef' // Angular ]; return componentPatterns.some(p => node.name.includes(p)); } };
This skill can leverage the following MCP servers:
| Server | Description | Use Case | |--------|-------------|----------| | playwright-mcp | Browser automation | Custom scenarios | | clinic.js | Node.js profiling | Alternative memory analysis |
This skill integrates with the following processes:
memory-leak-detection.js - Memory leak detection workflowsmemory-profiling-analysis.js - Comprehensive memory analysisWhen executing operations, provide structured output:
json{ "operation": "detect-leaks", "status": "completed", "scenario": "dashboard-leak-test", "results": { "leaksFound": 3, "totalLeakedSize": "2.5 MB", "leaks": [ { "type": "Detached HTMLDivElement", "count": 15, "totalSize": "1.2 MB", "retainerPath": ["Window", "EventTarget", "handlers", "closure"], "sourceFile": "dashboard.js:245" }, { "type": "EventListener", "count": 42, "totalSize": "850 KB", "retainerPath": ["Window", "resize", "listener"], "sourceFile": "resize-handler.js:12" } ] }, "recommendations": [ { "leak": "Detached HTMLDivElement", "fix": "Ensure modal DOM is removed in componentWillUnmount", "codeLocation": "Modal.tsx:89" } ], "reportPath": "./memlab-results/report/index.html" }
| Error | Cause | Resolution | |-------|-------|------------| | Chrome not found | Missing browser | Install Chrome or specify path | | Timeout exceeded | Slow page load | Increase timeout, check network | | OOM in analysis | Large heap | Increase Node.js memory limit | | No leaks found | Scenario too short | Increase iterations, longer actions |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 17,884 | 16,302 | -9% | 1 | 1 | 0% | 3,339 | 6,129 | +84% | 0 | 0 | — |
case-02 | fail→fail | 29,592 | 18,498 | -37% | 1 | 1 | 0% | 6,039 | 7,673 | +27% | 0 | 0 | — |
case-03 | fail→fail | 20,132 | 17,037 | -15% | 1 | 1 | 0% | 3,980 | 7,232 | +82% | 0 | 0 | — |
case-04 | pass→pass | 12,333 | 14,676 | +19% | 1 | 1 | 0% | 2,318 | 6,152 | +165% | 0 | 0 | — |
case-05 | pass→pass | 17,352 | 21,308 | +23% | 1 | 1 | 0% | 3,263 | 7,281 | +123% | 0 | 0 | — |
case-06 | pass→pass | 17,253 | 22,310 | +29% | 1 | 1 | 0% | 2,969 | 7,037 | +137% | 0 | 0 | — |
case-07 | fail→pass | 11,279 | 9,240 | -18% | 1 | 1 | 0% | 2,047 | 5,167 | +152% | 0 | 0 | — |
case-08 | fail→fail | 18,639 | 19,406 | +4% | 1 | 1 | 0% | 3,677 | 7,053 | +92% | 0 | 0 | — |
case-09 | pass→pass | 4,807 | 5,632 | +17% | 1 | 1 | 0% | 922 | 4,359 | +373% | 0 | 0 | — |
case-10 | fail→pass | 12,210 | 7,686 | -37% | 1 | 1 | 0% | 2,189 | 5,289 | +142% | 0 | 0 | — |
case-11 | pass→pass | 16,353 | 12,067 | -26% | 1 | 1 | 0% | 3,153 | 6,256 | +98% | 0 | 0 | — |
case-12 | fail→pass | 18,518 | 14,682 | -21% | 1 | 1 | 0% | 3,232 | 6,673 | +106% | 0 | 0 | — |
case-13 | fail→pass | 13,288 | 5,508 | -59% | 1 | 1 | 0% | 2,733 | 4,799 | +76% | 0 | 0 | — |
case-14 | pass→pass | 14,243 | 5,029 | -65% | 1 | 1 | 0% | 2,455 | 4,574 | +86% | 0 | 0 | — |
case-15 | fail→pass | 17,072 | 8,293 | -51% | 1 | 1 | 0% | 3,098 | 5,152 | +66% | 0 | 0 | — |
case-16 | fail→pass | 15,736 | 11,187 | -29% | 1 | 1 | 0% | 3,017 | 5,531 | +83% | 0 | 0 | — |
case-17 | pass→pass | 45,356 | 13,244 | -71% | 1 | 1 | 0% | 2,712 | 6,301 | +132% | 0 | 0 | — |
case-18 | fail→fail | 19,385 | 16,649 | -14% | 1 | 1 | 0% | 2,991 | 6,378 | +113% | 0 | 0 | — |
case-19 | fail→pass | 11,804 | 6,151 | -48% | 1 | 1 | 0% | 1,702 | 4,924 | +189% | 0 | 0 | — |
case-20 | fail→pass | 4,354 | 3,418 | -21% | 1 | 1 | 0% | 752 | 4,298 | +472% | 0 | 0 | — |
case-21 | fail→fail | 40,796 | 19,907 | -51% | 1 | 1 | 0% | 3,524 | 7,895 | +124% | 0 | 0 | — |
case-22 | fail→pass | 15,702 | 11,829 | -25% | 1 | 1 | 0% | 2,131 | 5,904 | +177% | 0 | 0 | — |
case-23 | pass→pass | 33,592 | 15,870 | -53% | 1 | 1 | 0% | 2,905 | 6,205 | +114% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +43 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.