Detecting Missing dispose() Calls
Every mixin covered in How to write Mixin Functions follows the same shape: it tracks the instances it creates in a _managed* array, and its dispose() override walks that array calling .destroy() (or .deactivateTrap(), or whatever the resource-specific teardown is) on each one before chaining to super.dispose(). This works — but only if dispose() is actually called on the owning Component in the first place. A Component that's removed from the DOM without ever having .dispose() called on it doesn't clean up anything: its event listeners stay registered, any WeakMap/Map registry entries pointing at it stay alive, and the objects it references can't be garbage collected.
This is a silent failure mode. Nothing throws. The application keeps working. Memory usage just climbs, a little at a time, every time a view is torn down without disposal — until, hours or days into a long-running session, the tab slows to a crawl or crashes.
The General Technique: Two Heap Snapshots
Chrome DevTools' Memory panel (Heap Snapshot mode) is the standard tool for this, and the workflow is the same regardless of framework:
- Open DevTools → Memory tab → Heap snapshot.
- Get the application into a stable baseline state, then take a snapshot ("Snapshot 1").
- Perform the action you suspect leaks — navigate to a view and back, open and close a modal, mount and unmount a component — several times in a row (repeating it makes the leak's growth pattern unmistakable in the next step).
- Take a second snapshot ("Snapshot 2").
- In Snapshot 2, switch the view dropdown from "Summary" to "Comparison", comparing against Snapshot 1. Sort by
# Delta(allocated minus freed) orRetained Size— either surfaces objects that grew in count without shrinking back down. - Look specifically for "Detached" DOM nodes — elements that are no longer in the visible page but are still retained in memory by something holding a reference to them. This is the single most common signature of a
dispose()-related leak in any DOM-based framework: an element left the page, but something (an event listener, a closure, a registry) never let go of it. - Click into a detached node's Retainers panel (bottom pane) to see why it's still alive — the retainer chain shows you the exact reference keeping it around.
What to Look For, Specifically
When the retainer chain leads back to your own code rather than a browser-internal structure, the usual culprits — in rough order of how often they turn out to be the cause — are:
- A mixin's
dispose()never got called at all. Check whether whatever removed theComponentfrom the tree (a router's view teardown, a conditional render, a manual.remove()) is also responsible for calling.dispose()on it — the framework does not do this automatically just because an element left the DOM. - A mixin's
dispose()override forgot to chain tosuper.dispose(). If a custom mixin overridesdispose()without callingsuper.dispose(...args), every mixin beneath it in the composition order never runs its own cleanup. This is easy to miss in a quick code read, since the override still looks like it's disposing something. - A
WeakMap/Mapregistry keyed by component identity was never normalized. As covered in Proxy and Raw Instance Identity, a registry that mixes raw and Proxy references as keys can end up holding a stale entry that the intended cleanup path never finds — because it's looking up the wrong identity to delete. - An event listener was registered outside of the framework's own delegation. Anything added with a raw
addEventListenerinsidecreated()oronAfterRender()needs a matchingremoveEventListenerindispose(). Listeners registered through the framework's own event delegation (theeventsgetter) don't have this problem, since delegation happens at the document level and isn't tied to a per-instance listener.
The steps above (comparison view, Detached nodes, retainer chains) describe Chrome DevTools' general-purpose heap snapshot workflow, which applies to any DOM-based application regardless of framework. What a specific geajs leak looks like in the retainer tree — which class names show up, how many hops the chain takes — has not been captured and verified against a real reproduction as of this writing. If you're debugging a real leak, treat the retainer chain DevTools shows you as the source of truth over any specific example in this chapter.
A Habit Worth Building
Rather than debugging a leak after the fact, get in the habit of pairing every place that constructs and mounts a Component with the code path that will eventually dispose of it, and write it down at the same time you write the construction — before you've had a chance to forget. A Component created without an obvious answer to "who calls .dispose() on this, and when?" is a leak waiting to happen.