Claude
Skills
Sign in
Back

memory-leak-detector

Included with Lifetime
$97 forever

Detect JavaScript memory leaks by measuring JS heap size and DOM node count across repeated interactions. Uses CDP HeapProfiler.collectGarbage for forced GC, analyzes growth trends, and reports leak rate per iteration.

Ads & Marketing

What this skill does


# Memory Leak Detector

Measure JavaScript heap usage and DOM node count across repeated user
interactions to detect memory leaks. Forces garbage collection between
measurements via CDP, then analyzes whether growth is monotonic and computes
the leak rate per iteration.

## When to Use

- A page becomes sluggish after extended use (SPA with route changes).
- Opening and closing a modal/dialog repeatedly causes growing memory.
- A list or table component leaks detached DOM nodes on re-render.
- You need quantitative evidence of a memory leak before deep-diving with
  Chrome DevTools heap snapshots.

## Prerequisites

- **Playwright MCP server** connected and responding.
- **Chromium-based browser** required. Two Chromium-specific features are used:
  - `performance.memory` (non-standard, Chromium only) for JS heap metrics.
  - CDP `HeapProfiler.collectGarbage` via `browser_run_code` for forced GC.
- The skill degrades gracefully if `performance.memory` is unavailable (falls
  back to DOM node count only).

## Workflow

### Step 1 -- Navigate to the Target Page

```
browser_navigate({ url: "<target_url>" })
```

Wait for the page to stabilize:

```
browser_wait_for({ time: 3 })
```

### Step 2 -- Capture Baseline

Call `browser_run_code` to create a CDP session, force garbage collection, and
record baseline heap and DOM metrics.

```javascript
browser_run_code({
  code: `async (page) => {
    // Create CDP session for GC control
    const client = await page.context().newCDPSession(page);
    await client.send('HeapProfiler.collectGarbage');
    // Store client reference for later use
    // (We will create a new session each time since we cannot persist it)

    // Wait for GC to complete
    await page.waitForTimeout(500);

    // Measure baseline
    const baseline = await page.evaluate(() => {
      const result = {
        timestamp: performance.now(),
        domNodeCount: document.querySelectorAll('*').length,
        hasPerformanceMemory: !!performance.memory
      };
      if (performance.memory) {
        result.usedJSHeapSize = performance.memory.usedJSHeapSize;
        result.totalJSHeapSize = performance.memory.totalJSHeapSize;
        result.jsHeapSizeLimit = performance.memory.jsHeapSizeLimit;
      }
      return result;
    });

    await client.detach();
    return baseline;
  }`
})
```

Record the baseline values for comparison.

### Step 3 -- Define the Interaction to Repeat

Before looping, identify the interaction sequence that you suspect leaks
memory. Common patterns:

- **Route change loop**: navigate to a sub-page, then back.
- **Modal open/close**: open a dialog, interact with it, close it.
- **List manipulation**: add items, remove them, repeat.
- **Search/filter cycle**: type a query, clear it, repeat.

Take a `browser_snapshot` to identify the interactive elements and their refs.

### Step 4 -- Repeat Interaction N Times with Measurements

Call `browser_run_code` with the interaction loop. Replace the interaction
section with the actual steps for your use case.

```javascript
browser_run_code({
  code: `async (page) => {
    const iterations = 10;  // Adjust as needed
    const measurements = [];

    for (let i = 0; i < iterations; i++) {
      // ===== YOUR INTERACTION HERE =====
      // Example: open and close a modal
      // await page.click('button#open-modal');
      // await page.waitForTimeout(500);
      // await page.click('button.modal-close');
      // await page.waitForTimeout(500);

      // Example: navigate and return
      // await page.click('a[href="/details"]');
      // await page.waitForTimeout(1000);
      // await page.goBack();
      // await page.waitForTimeout(1000);
      // ===== END INTERACTION =====

      // Force GC
      const client = await page.context().newCDPSession(page);
      await client.send('HeapProfiler.collectGarbage');
      await page.waitForTimeout(500);
      await client.detach();

      // Measure
      const measurement = await page.evaluate((iteration) => {
        const result = {
          iteration: iteration + 1,
          timestamp: performance.now(),
          domNodeCount: document.querySelectorAll('*').length
        };
        if (performance.memory) {
          result.usedJSHeapSize = performance.memory.usedJSHeapSize;
          result.totalJSHeapSize = performance.memory.totalJSHeapSize;
        }
        return result;
      }, i);

      measurements.push(measurement);
    }

    return measurements;
  }`
})
```

### Step 5 -- Analyze Results

Call `browser_evaluate` to compute leak metrics from the measurements array.
Pass the baseline and measurements data collected from the previous steps.

```javascript
browser_evaluate({
  function: `() => {
    // Paste baseline and measurements from previous steps
    const baseline = __BASELINE__;      // Replace with actual baseline object
    const measurements = __MEASUREMENTS__; // Replace with actual measurements array

    const hasHeap = baseline.hasPerformanceMemory;
    const n = measurements.length;
    if (n < 2) return { error: 'Need at least 2 measurements' };

    // --- Heap analysis ---
    let heapAnalysis = null;
    if (hasHeap) {
      const heapSizes = [baseline.usedJSHeapSize, ...measurements.map(m => m.usedJSHeapSize)];
      const heapGrowths = [];
      let monotonic = true;
      for (let i = 1; i < heapSizes.length; i++) {
        const growth = heapSizes[i] - heapSizes[i - 1];
        heapGrowths.push(growth);
        if (growth < 0) monotonic = false;
      }
      const totalGrowth = heapSizes[heapSizes.length - 1] - heapSizes[0];
      const growthPercent = (totalGrowth / heapSizes[0]) * 100;
      const avgGrowthPerIteration = totalGrowth / n;

      heapAnalysis = {
        baselineBytes: heapSizes[0],
        finalBytes: heapSizes[heapSizes.length - 1],
        totalGrowthBytes: totalGrowth,
        totalGrowthMB: Math.round(totalGrowth / 1048576 * 100) / 100,
        growthPercent: Math.round(growthPercent * 100) / 100,
        avgGrowthPerIterationBytes: Math.round(avgGrowthPerIteration),
        avgGrowthPerIterationKB: Math.round(avgGrowthPerIteration / 1024 * 100) / 100,
        isMonotonic: monotonic,
        verdict: growthPercent > 10 ? 'PROBABLE_LEAK' :
                 growthPercent > 5 ? 'POSSIBLE_LEAK' : 'LIKELY_OK',
        perIterationGrowths: heapGrowths.map(g => Math.round(g / 1024 * 100) / 100 + ' KB')
      };
    }

    // --- DOM node analysis ---
    const domCounts = [baseline.domNodeCount, ...measurements.map(m => m.domNodeCount)];
    const domGrowths = [];
    let domMonotonic = true;
    for (let i = 1; i < domCounts.length; i++) {
      const growth = domCounts[i] - domCounts[i - 1];
      domGrowths.push(growth);
      if (growth < 0) domMonotonic = false;
    }
    const totalDomGrowth = domCounts[domCounts.length - 1] - domCounts[0];
    const avgDomGrowthPerIteration = totalDomGrowth / n;

    const domAnalysis = {
      baselineNodes: domCounts[0],
      finalNodes: domCounts[domCounts.length - 1],
      totalGrowth: totalDomGrowth,
      avgGrowthPerIteration: Math.round(avgDomGrowthPerIteration * 100) / 100,
      isMonotonic: domMonotonic,
      verdict: avgDomGrowthPerIteration > 10 ? 'DOM_LEAK' :
               avgDomGrowthPerIteration > 0 && domMonotonic ? 'POSSIBLE_DOM_LEAK' : 'LIKELY_OK',
      perIterationGrowths: domGrowths
    };

    return {
      iterations: n,
      heapAnalysis,
      domAnalysis,
      overallVerdict: (heapAnalysis && heapAnalysis.verdict === 'PROBABLE_LEAK') || domAnalysis.verdict === 'DOM_LEAK'
        ? 'MEMORY LEAK DETECTED'
        : (heapAnalysis && heapAnalysis.verdict === 'POSSIBLE_LEAK') || domAnalysis.verdict === 'POSSIBLE_DOM_LEAK'
        ? 'POSSIBLE MEMORY LEAK -- investigate further'
        : 'NO LEAK DETECTED'
    };
  }`
})
```

Alternatively, perform the analysis directly in your response by examining the
measurement data -- no `browser_evaluate` call required if you have the raw
numbers.

### S

Related in Ads & Marketing