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

react-memory-leak-detector

v1.1.0

Published

Stop hunting memory leaks with heap snapshots. This babel plugin tags every React component and hook with a uniquely-named marker, then a runtime tracker uses WeakRef + FinalizationRegistry to warn you, live in the console, the moment something leaks.

Downloads

311

Readme

react-memory-leak-detector

Live memory-leak detection for React components and hooks. No heap snapshot required.

The moment a component is unmounted but still retained — by a closure, event listener, timer, or subscription — you know about it: as a leak event you can subscribe to and forward anywhere (a debug overlay, your own logger, Sentry), or as a live console warning. Under the hood, a babel plugin tags every component and hook, and a runtime tracker watches those tags with WeakRef + FinalizationRegistry.

Dev-only. Zero impact on production bundles.

Installation

npm install --save-dev react-memory-leak-detector

Setup

The detector has two pieces: a Babel plugin that injects the markers, and a runtime you load once in dev.

1. Add the Babel plugin

Enable it in development only. It runs through Babel, so it plugs into any Babel-based build.

Vite + @vitejs/plugin-react The Babel-based React plugin exposes a babel option — pass the plugin there, gated to development:

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import heapMarkers from "react-memory-leak-detector/babel-plugin";

export default defineConfig(({ mode }) => ({
  plugins: [
    react({
      babel: {
        plugins:
          mode === "development"
            ? [
                [
                  heapMarkers,
                  {
                    /* options */
                  },
                ],
              ]
            : [],
      },
    }),
  ],
}));

Requires @vitejs/plugin-react v5. v6+ switched to Oxc and removed the babel option, so it isn't supported yet — pin to v5 (npm i -D @vitejs/plugin-react@^5), which is Babel-based and still supports Vite 8. (First-class v6 support is planned.)

Webpack / Next.js / standard Babel Add the plugin to your .babelrc, babel.config.js, or babel-loader options for the development environment:

// .babelrc
{
  "env": {
    "development": { "plugins": ["react-memory-leak-detector/babel-plugin"] }
  }
}

2. Import the runtime

Load the runtime once at the very top of your entry (src/main.tsx, src/index.tsx, _app.tsx, …), dynamically imported so it stays out of your production bundle:

// Webpack / Next.js
if (process.env.NODE_ENV === "development") {
  import("react-memory-leak-detector/runtime");
}

// Vite
if (import.meta.env.DEV) {
  import("react-memory-leak-detector/runtime");
}

TypeScript

Type declarations ship with the package for both entry points (/babel-plugin, /runtime), so no ambient declare module shims are needed. The runtime augments the global Window type — window.__heapTracker and window.__heapTrackerOptions are typed for you. The Babel plugin's options are exported as HeapMarkersOptions:

import type { HeapMarkersOptions } from "react-memory-leak-detector/babel-plugin";

Usage

The tracker installs window.__heapTracker on dev page load.

Subscribing to leaks

Use subscribe to forward stale-leak events wherever they're most useful — your own logger, a debug overlay, Sentry, etc. The listener only fires for stale leaks (instances unmounted ≥ leakAgeMs ago and still reachable), never for currently-mounted or recently-unmounted components.

const unsubscribe = window.__heapTracker.subscribe((event) => {
  // event: { component, stale, live, leakAgeMs, at }
  console.log(`Leak in ${event.component}: ${event.stale} stale instance(s)`);
  // e.g. Sentry.captureMessage(`heap-leak:${event.component}`, { extra: event });
});

// later
unsubscribe();

Event shape:

| Field | Type | Description | | ----------- | -------- | --------------------------------------------------------------------------------- | | component | string | Component / hook name. Matches the ComponentName$Heap marker in heap snapshots. | | stale | number | Instances unmounted ≥ leakAgeMs ago that are still reachable. | | live | number | Total reachable instances (mounted + recently-unmounted + leaked). | | leakAgeMs | number | Threshold used to classify an instance as stale. | | at | number | Date.now() when the event was fired. |

Firing is gated by the same suspectThreshold and warnCooldownMs as the console warning below, so you won't get spammed. To receive events without the console warnings, call configure({ logging: false }).

Console warnings

On dev page load you'll see:

[heap-leak] tracker installed. Run window.__heapTracker.report() for a live table.

Warnings fire automatically as console.warn, debounced to once per 30s per component:

[heap-leak] Suspected leak: GapsByPriorityCard — 1 instance(s) unmounted >10s ago still retained (live 1 total)

Reading the numbers: live vs stale

Both count _heap_ instances still reachable in JS:

  • live — total reachable instances. Includes currently-mounted, recently-unmounted (GC pending), and leaked.
  • stalelive — only those where markUnmounted fired ≥10s ago. These are the leak suspects.

The leak signal is purely stale; live is context.

| Scenario | live | stale | | ------------------------------------ | ---- | ----- | | 80 cards on screen | 80 | 0 | | Just navigated away, <10s ago | 0–80 | 0 | | Unmounted but listener still holds 1 | 1+ | 1+ |

Finding the leak

Once a component shows up as stale, take a heap snapshot in Chrome DevTools and filter for ComponentName$Heap — here, LeakyTimeout$Heap:

Heap snapshot: LeakyTimeout$Heap instances still retained, with the Retainers panel showing a DOMTimer holding them

Each ComponentName$Heap row is one component instance still alive in memory. If instances linger after the component unmounted, that's the leak. Select one and read the Retainers panel bottom-up to see what is holding it: here the chain runs through a DOMTimerScheduledAction, i.e. an uncleared setTimeout/setInterval is pinning LeakyTimeout's scope (its _heap_ marker). Clear that timer on unmount and the instances disappear.

API

| Call | Purpose | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | window.__heapTracker.report() | console.table of every component with live instances; also returns the array. | | window.__heapTracker.configure() | Configure runtime options. Example: configure({ logging: false }) disables all console outputs while continuing tracking. | | window.__heapTracker.sweep() | Force an immediate sweep (otherwise runs every 2s). | | window.__heapTracker.forceGc() | window.gc?.() + re-sweep. Needs Chrome started with --js-flags="--expose-gc". Use it to confirm a flag isn't just GC lag. | | window.__heapTracker.subscribe() | Subscribe to stale-leak events (see Subscribing to leaks). Fires only for stale leaks, gated by the same suspectThreshold + warnCooldownMs as the console warning. Returns an unsubscribe function. |

Configuration

heapMarkers({
  include: /\.[tj]sx?$/, // file path regex
  excludeNames: [/^useTranslation$/], // names that skip ALL instrumentation
  excludeUnmountTracking: [], // names that keep the heap marker but skip
  // the synthetic useEffect (use for components
  // managed by <Activity mode="hidden">)
  trackHooks: true, // when false, only components are instrumented
  skipServerComponents: false, // when true, skip files without "use client"
  logging: true, // when false, disables automatic console warnings
  leakAgeMs: 10000, // time in ms before an unmounted component is considered leaked
  suspectThreshold: 1, // minimum number of stale instances before warning
  sweepIntervalMs: 2000, // how often the GC sweep checks for leaks
  warnCooldownMs: 30000, // how long to wait before warning about the SAME component again
});

All options are optional with sensible defaults.

include, excludeNames, excludeUnmountTracking, trackHooks, and skipServerComponents govern what the Babel plugin instruments at build time. The rest (logging, leakAgeMs, suspectThreshold, sweepIntervalMs, warnCooldownMs) are runtime behaviors — when you set any of them on the plugin, it injects window.__heapTrackerOptions so the runtime picks them up on load. Equivalent ways to configure the runtime:

// Before the runtime is imported (e.g. top of main.tsx):
window.__heapTrackerOptions = { leakAgeMs: 5000, logging: false };

// Or any time after it has loaded:
window.__heapTracker?.configure({ leakAgeMs: 5000 });

How it works

  1. The babel plugin injects a uniquely-named _heap_ marker into every component/hook — searchable as ComponentName$Heap in Chrome DevTools heap snapshots.

  2. The runtime tracker wraps each marker in a WeakRef and registers it with a FinalizationRegistry. A sweep every 2s checks which markers are still reachable.

  3. To know when a component actually unmounts, the babel plugin also injects a synthetic useEffect into every component and hook:

    __heap_useEffect(() => {
      window.__heapTracker?.markMounted(_heap_);
      return () => window.__heapTracker?.markUnmounted(_heap_);
    }, []);

    Imported under a renamed alias so it can't collide with user code. A mount counter handles React StrictMode's double-invoke correctly.

  4. If a _heap_ is still reachable in JS ≥10s after its component unmounted, something is leaking it → console warning.

Compatibility

| React feature | Status | | ----------------------------------------- | ------------------------------------------ | | React 17 / 18 function components & hooks | ✅ | | React 18 concurrent, Suspense, StrictMode | ✅ | | SSR (effects no-op server-side) | ✅ | | React Compiler | ✅ likely; warrants a CI snapshot test | | <Activity mode="hidden"> | ⚠️ use excludeUnmountTracking to opt out | | React Server Components | ⚠️ use skipServerComponents: true | | Class components | ❌ not instrumented (PRs welcome) |

License

MIT