Performance and DevTools
Find a memory leak
Use heap snapshots and allocation tools to find objects that remain reachable after their feature is closed.
A good leak test repeats a lifecycle that should bring the page back to where it started. Open something, close it, check that memory returns to the same level.
Suppose opening a dialog creates DOM nodes, attaches listeners, and loads a large data model. Here’s the procedure:
- Load the page and take a baseline heap snapshot in the Memory panel.
- Open and close the dialog five times.
- Click the trash-can icon to collect garbage. This makes snapshots comparable. Your app can’t call this, it’s a DevTools button only.
- Take a second snapshot and switch the view to “Comparison” against the first.
- Repeat steps 2 to 4 once more and see whether the same object groups keep growing.
What counts as evidence
A bigger heap on its own proves nothing. The browser may hold on to capacity it plans to reuse. Your app may fill an intentional cache on first use. Collection timing varies between runs.
The evidence you want is a set of objects whose feature is closed but that are still reachable after repeated cycles. In the Comparison view, sort by the ”# Delta” column. Your dialog’s class showing +5 after five open-close cycles is the smoking gun.
Follow the retaining path
The snapshot often shows “Detached HTMLDivElement” entries. Those are a clue, not the cause. Something is holding those nodes, and you need to find what.
Click one of the leaked objects. The Retainers pane at the bottom shows the retaining path: the chain of references from a GC root down to this object. Read it from the bottom up. It usually ends at one of these:
- an event listener on
windowordocument - a timer created with
setInterval()that was never cleared - a closure kept alive by a callback somewhere
- an entry in a cache or an array that still points at the object
That last link in the chain is the code you need to fix.
Fix ownership at the boundary
The dialog created the resources when it opened, so the dialog must release them when it closes:
- remove listeners, or register them with an
AbortControllersignal and abort it - call
disconnect()on observers - clear timers and cancel animation frames
- unsubscribe from data sources
- give caches a size limit and an expiry
- drop references to detached DOM trees
Prove it
Run the same five cycles after the fix and compare again. The strongest evidence is that the instance count stays flat while the dialog still works. If the delta is zero but the dialog broke, you removed too much. Clean up only what the dialog owns.
Lesson completed