npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@elliemae/encw-heap-doctor

v26.3.2

Published

CLI tool to analyze Chrome heap snapshots and find memory leak root causes

Readme

heap-doctor

Analyzes Chrome V8 heap snapshots to help developers find and fix memory leaks in web applications. It parses .heapsnapshot files, groups leaked objects by type, walks retainer paths to identify the root JS reference holding objects in memory, matches known leak patterns, and generates prioritized fix suggestions — plus a ready-to-use AI prompt you can send to any LLM of your choice.

Key Features

  • Parses multi-GB heap snapshots using off-heap typed arrays (handles files up to 8 GB)
  • Detects common leak types — detached DOM nodes, large closures, stale cache references
  • Traces retainer chains to the exact JS reference preventing garbage collection
  • Pattern matching for known leak causes (GTM data layer, AngularJS scope/watchers, React fiber trees, event listeners, timers, observers)
  • Snapshot comparison — diff two snapshots to find growth in leaked objects
  • Scenario runner — automate browser flows with Playwright to capture and compare snapshots
  • Fix priority ranking across leak groups sharing the same root cause
  • AI prompt generationgenerateAiPrompt() produces a self-contained prompt from the analysis report that you can send to any LLM (Claude, GPT-4, Gemini, etc.)
  • Markdown reports as the sole output format — clean, shareable, version-controllable

Architecture

HeapDoctor exposes a single public class that composes all internal services. Three methods cover every use case: analyse a single snapshot, compare two snapshots, or run an automated browser scenario.

flowchart TD
    subgraph publicAPI [HeapDoctor Public API]
        analyse["analyse(filePath)"]
        compare["compare(before, after)"]
        runScenario["runScenario(scenario)"]
    end

    subgraph core [Core]
        SnapshotParser["SnapshotParser"]
        Snapshot["Snapshot"]
    end

    subgraph analysis [Analysis]
        LeakDetector["LeakDetector"]
        RetainerTracer["RetainerTracer"]
        PatternMatcher["PatternMatcher"]
        SuggestionGenerator["SuggestionGenerator"]
        SnapshotComparer["SnapshotComparer"]
    end

    subgraph scenario [Scenario]
        PlaywrightRunner["PlaywrightScenarioRunner"]
        Playwright["Playwright Page"]
    end

    subgraph prompts [AI Prompt]
        PromptGenerator["generateAiPrompt()"]
        Consumer["Your LLM (Claude / GPT-4 / Gemini)"]
    end

    subgraph reporting [Reporting]
        MarkdownGen["MarkdownReportGenerator"]
    end

    analyse --> SnapshotParser
    SnapshotParser --> Snapshot
    Snapshot --> LeakDetector
    LeakDetector --> RetainerTracer
    RetainerTracer --> PatternMatcher
    PatternMatcher --> SuggestionGenerator
    SuggestionGenerator --> MarkdownGen
    SuggestionGenerator --> PromptGenerator
    PromptGenerator --> Consumer

    compare --> SnapshotParser
    compare --> SnapshotComparer
    SnapshotComparer --> LeakDetector

    runScenario --> PlaywrightRunner
    PlaywrightRunner --> Playwright
    PlaywrightRunner --> compare

Analysis Pipeline

| Stage | Class | What it does | | ------------- | -------------------------- | ------------------------------------------------------------------------------------------- | | Parse | SnapshotParser | Reads .heapsnapshot as raw Buffer into off-heap typed arrays for multi-GB files | | Detect | LeakDetector | Finds detached DOM, large closures, large arrays; groups by tag, ranks by retained size | | Trace | RetainerTracer | Walks retainer edges via priority BFS to build JS-level chains to GC roots | | Match | PatternMatcher | Matches chains against known leak patterns (GTM, AngularJS, React, event listeners, timers) | | Suggest | SuggestionGenerator | Generates severity-ranked fix suggestions from patterns and chain context | | Compare | SnapshotComparer | Diffs two snapshots: new/removed nodes, retained size delta, new leak groups | | Scenario | PlaywrightScenarioRunner | Runs a Playwright browser scenario, captures before/after heap snapshots via CDP | | Report | MarkdownReportGenerator | Renders structured results as a markdown report | | AI Prompt | generateAiPrompt() | Serializes all findings into a self-contained prompt for any LLM |

File Structure

lib/
  index.ts                          Re-exports HeapDoctor and public types
  heapDoctor.ts                     Public HeapDoctor facade class
  types/
    index.ts                        Result<T>, branded IDs, common types
    report.ts                       AnalysisReport, ComparisonReport, ScenarioReport
    leak.ts                         LeakGroup, RetainerChain, Suggestion, etc.
    scenario.ts                     HeapDoctorScenario (uses Playwright Page)
  errors/
    domainError.ts                  Base DomainError class
    parseError.ts                   Snapshot parsing errors
    scenarioError.ts                Scenario execution errors
  core/
    snapshot.ts                     Snapshot class (typed V8 heap representation)
    snapshotParser.ts               SnapshotParser class (Buffer-based parser)
  analysis/
    leakDetector.ts                 LeakDetector (detached DOM, closures, arrays)
    retainerTracer.ts               RetainerTracer (BFS retainer path walking)
    patternMatcher.ts               PatternMatcher (known leak pattern detection)
    suggestionGenerator.ts          SuggestionGenerator (fix generation)
    snapshotComparer.ts             SnapshotComparer (diff two snapshots)
  scenario/
    playwrightScenarioRunner.ts     PlaywrightScenarioRunner (CDP snapshot capture)
  prompts/
    promptGenerator.ts              generateAiPrompt() (LLM-ready prompt builder)
  reporting/
    markdownReportGenerator.ts      MarkdownReportGenerator (markdown output)
  utils/
    formatUtils.ts                  FormatUtils (formatBytes, truncate, escapeHtml)
bin/
  heap-doctor.ts                    CLI entry point (composition root)

Installation

npm install @elliemae/encw-heap-doctor

For scenario support (automated browser flows):

npm install playwright

Programmatic API

HeapDoctor is designed as a library-first API. All methods return Result<T> for type-safe error handling.

1. Single Snapshot Analysis

Analyse a .heapsnapshot file for memory leaks:

import { HeapDoctor } from '@elliemae/encw-heap-doctor';

const doctor = new HeapDoctor({ topN: 5 });
const result = await doctor.analyse('app.heapsnapshot');

if (result.ok) {
  console.log(result.value.markdown); // full markdown report
  console.log(result.value.leakResults); // structured leak data
  console.log(result.value.fixPriority); // ranked fix priorities
} else {
  console.error(result.error.code, result.error.message);
}

2. Snapshot Comparison

Compare two snapshots to find what grew between them:

import { HeapDoctor } from '@elliemae/encw-heap-doctor';

const doctor = new HeapDoctor();
const result = await doctor.compare(
  'before.heapsnapshot',
  'after.heapsnapshot',
);

if (result.ok) {
  const { delta } = result.value;
  console.log(`New nodes: +${delta.newNodeCount}`);
  console.log(`Removed:   -${delta.removedNodeCount}`);
  console.log(`Size delta: ${delta.retainedSizeDelta} bytes`);

  for (const group of delta.newLeakGroups) {
    console.log(`New leak: ${group.label} x${group.count}`);
  }

  // Full markdown comparison report
  console.log(result.value.markdown);
}

3. Automated Scenario (Playwright)

Define a browser scenario and let HeapDoctor capture + compare snapshots automatically:

import { HeapDoctor } from '@elliemae/encw-heap-doctor';
import type { HeapDoctorScenario } from '@elliemae/encw-heap-doctor';

const scenario: HeapDoctorScenario = {
  url() {
    return 'https://myapp.com';
  },

  async setup(page) {
    await page.goto('https://myapp.com/login');
    await page.getByPlaceholder('Email').fill('[email protected]');
    await page.getByPlaceholder('Password').fill('password');
    await page.getByRole('button', { name: 'Sign In' }).click();
    await page.waitForURL('**/dashboard/**');
  },

  async action(page) {
    await page.getByRole('button', { name: 'Open Modal' }).click();
    await page.waitForSelector('.modal-content');
  },

  async back(page) {
    await page.getByRole('button', { name: 'Close' }).click();
    await page.waitForSelector('.modal-content', { state: 'hidden' });
  },

  repeat() {
    return 3; // repeat action->back cycle 3 times
  },
};

const doctor = new HeapDoctor();
const result = await doctor.runScenario(scenario);

if (result.ok) {
  console.log(`Snapshots: ${result.value.snapshotPaths.join(', ')}`);
  console.log(result.value.markdown);
}

The runner captures a "before" snapshot after setup(), executes action() -> back() for repeat() cycles, then captures an "after" snapshot. Both are compared and a full report is generated.

4. AI Prompt Generation

generateAiPrompt() converts any analysis or comparison report into a self-contained prompt string. Paste it into Claude, GPT-4, Gemini, or any LLM — no API keys or registry access required on heap-doctor's side.

import { HeapDoctor, generateAiPrompt } from '@elliemae/encw-heap-doctor';

const doctor = new HeapDoctor({ topN: 5 });
const result = await doctor.analyse('app.heapsnapshot');

if (result.ok) {
  const prompt = generateAiPrompt(result.value);

  // Option A: print to stdout and copy-paste into any chat UI
  console.log(prompt);

  // Option B: write to a file and attach to your LLM of choice
  writeFileSync('heap-doctor-prompt.txt', prompt);

  // Option C: pass directly to your own AI integration
  const aiResponse = await myLlmClient.complete(prompt);
}

Works identically for comparison reports:

const result = await doctor.compare(
  'before.heapsnapshot',
  'after.heapsnapshot',
);
if (result.ok) {
  const prompt = generateAiPrompt(result.value);
  // prompt includes the delta summary (new nodes, size change) as context
}

The generated prompt includes:

  • Full context header instructing the LLM to act as a memory leak specialist
  • Per-leak sections: label, count, retained size, matched pattern, root cause, retainer chains, edge paths, existing suggestions
  • Structured output format requesting LEAK #N sections with specific action items

CLI Usage

# Build first
npm run build

# Analyse a single snapshot
heap-doctor analyse app.heapsnapshot

# Compare two snapshots
heap-doctor compare before.heapsnapshot after.heapsnapshot

# Run a scenario file
heap-doctor scenario my-scenario.ts

# Write report to file instead of stdout
heap-doctor analyse app.heapsnapshot -o report.md

CLI Options

| Flag | Description | Default | | --------------------- | ----------------------------------------------- | ------- | | -n, --top <number> | Number of top leak groups to report | 5 | | -o, --output <path> | Write markdown report to file (default: stdout) | stdout | | -V, --version | Print the version number | -- | | -h, --help | Show help | -- |

Subcommands

| Command | Description | | -------------------------- | -------------------------------------------------------- | | analyse <file> | Analyse a single heap snapshot | | compare <before> <after> | Compare two heap snapshots | | scenario <scenario-file> | Run a Playwright scenario and compare captured snapshots |

Headed Mode (Scenario)

To watch the browser during scenario execution:

HEADLESS=false npx tsx demo/scenario-run.ts

Example Report Output

Below is a single leak section from a generated markdown report:


MEMORY LEAK DETECTED

Node Detached <span>

Retained Memory 1370.16 MB

Retaining Function gtmDataLayer in Array

Leak Pattern Google Tag Manager Data Layer

Retainer Chain

(GC roots)
└ (Eternal handles)
   └ 307
      └ DOMStringMap()
         └ context
            └ extension
               └ gtmDataLayer in Array
                  └ [42] in Object
                     └ gtm.element in <button id="header-app-switcher" ...>
                        └ __reactFiber$kau10t4ajn9 in nd
                           └ return in nd
                              └ memoizedState in Object
                                 └ <span class="em-ds-popover__arrow" ...>

Suggested Fix

  • Configure GTM to not capture element references in click/form triggers
  • Periodically trim stale entries: window.dataLayer = window.dataLayer.slice(-50)
  • Use CSS selectors or element IDs in GTM triggers instead of element references
  • Add cleanup that nullifies gtm.element on stale dataLayer entries

License

MIT