react-memory-detective

Changelog

Every release, what changed and why โ€” including the mistakes, because the reasoning is the part worth reading when you are deciding whether to upgrade.

0.1.0 #

First release. Dev-time only, zero runtime dependencies, and it refuses to run in a production build. The version says preview and means it: the diagnoses it makes are ones it can defend, and it stays quiet about everything else.

Added

capability before any of it was built. Its central finding shaped the design: the browser memory APIs are the *weakest* signal available โ€” performance.memory is Chrome-only and deliberately quantised, and measureUserAgentSpecificMemory() needs cross-origin isolation most apps do not have. So the resource registry is the spine, and memory figures are supporting evidence.

handles in a WeakMap so tracking a resource can never be the reason it stays alive.

not a cleanup) from an uncleared setInterval (never resolves itself).

different function reference, and a different capture flag. Both are stated as fact rather than suspicion, because both references are observed directly.

one created while some component merely happened to be mounted has no owner, and the tool says so.

MutationObserver, ResizeObserver, IntersectionObserver. All seven share one shape (constructed, released by a single method) and one implementation. A socket the *peer* closes is marked self-resolved rather than blamed on the component, and instanceof keeps working so instrumentation cannot change application behaviour.

of which must stay quiet. It was mutation-tested rather than trusted: one deliberate break was caught, a second was not, and the test that should have caught it turned out to wait long enough for the timer to fire and so never reached the branch it protected. Fixed, and the mutation is now caught.

so configuring it silently did nothing and the cap was always the default.

written for it. Effects run after render, so at the moment a setInterval is created nothing in the runtime says whose effect is running โ€” a resource created in an ordinary useEffect had no owner, and an unattributed resource is not reported at all, because guessing the nearest mounted component is the behaviour this project exists to avoid. Verified in a browser: with the plugin, the six-component fixture app reports exactly its three real bugs and nothing else; without it, it reported one, unattributed. The compiler stays in the build and is asserted never to reach a runtime bundle.

assertion that no finding ever states a leak as a conclusion.

project and exercises ESM, CJS, every subpath entry, the plugin, and a type-check under both bundler and node16 resolution. It found two packaging bugs on its first run that every test in this repo had passed.

Changed

fixture app accumulated 52 for three bugs โ€” the same unreadable-at-scale mistake the console reporter was already redesigned to avoid. Findings are keyed by component, resource type and source location, and carry an occurrences count. When repetition proves a leak the earlier low-confidence suspicion is dropped rather than listed beside it, and a retained listener that a mismatch already explains is reported once, as the mismatch โ€” the finding that names the fix.

install @types/babel__core to type-check their own application.

Fixed

Five of the entries below came from one afternoon pointing this at Excalidraw โ€” a real application โ€” after the test suite was green and the fixture app was perfect. That is the whole argument for the exercise, and it is now a release criterion rather than a good intention.

the stranded listener was assumed to be the first one registered for that event โ€” which in the fixture app was always the right one, because it was the only one. In a real application several listeners share an event, so the finding pointed confidently at a stranger's listener. Attribution now comes from the owner scope of the component actually cleaning up (effect *cleanups* are scoped for this reason), and when it genuinely cannot be determined the mismatch is still reported as a fact with no component named. Guessing was the one thing this tool is not allowed to do.

two registrations for the same event looked ambiguous. They belong to the same component, so the attribution is certain.

Excalidraw, and a component in seven silently unattributed looks exactly like a tool that found nothing. Coverage measured on that codebase went from 77% to 89%.

genuinely malformed: Babel allows file: null and readonly arrays where Rollup does not, so the map is now rebuilt rather than passed through. Any project that type-checks its vite.config.ts โ€” Excalidraw does โ€” could not add the plugin at all.

?t= cache-busting query, so one file appeared as two and each source line was a paragraph.

receiver; jsdom tolerates this and Chrome does not, so the whole fixture app failed to start while every test stayed green. This is the second project where only real-browser contact caught a class of bug that fixtures could not.

which implied a figure existed and had merely been omitted.