Find a Retained Detached DOM Node With Heap Snapshots
Trace a detached preview from its DOM node to an application Map, repair cleanup, and verify repeated open-and-close cycles with heap snapshots.
Removing a preview from the page does not remove every reference to it. A draft registry can still own the preview, which still owns its DOM node. This guide follows that specific chain, fixes the registry cleanup, and repeats the same action to check the result.
A slow page alone does not prove a memory leak. Use this test when a repeated open-and-close action leaves objects behind. The course lesson on reachability and listener cleanup covers the underlying rules; here the job is to identify one retained detached node.
Prepare a controlled reproduction
Save the complete example below as detached-preview.html and open it in a current Chromium browser. Chrome calls its profiler Memory; Edge has a similar panel. Each click mounts and removes ten previews synchronously, so you will not see them flash. Keep the repair checkbox off for the first run.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Detached preview investigation</title>
<style>
body { font: 1rem/1.5 system-ui; max-width: 44rem; margin: 2rem auto; padding: 0 1rem; }
button, label { margin: .5rem .5rem .5rem 0; }
button { font: inherit; padding: .5rem; }
output { display: block; }
</style>
<h1>Preview lifecycle test</h1>
<label><input id="repair" type="checkbox"> Delete the record on close</label>
<button id="cycle" type="button">Open and close 10 previews</button>
<output id="status" aria-live="polite">Retained records: 0</output>
<div id="host"></div>
<script>
window.previewState = { records: new Map(), nextId: 0 };
class DetachedPreview {
constructor(id) {
this.id = id;
this.root = document.createElement("section");
this.root.dataset.preview = String(id);
this.root.textContent = `Draft preview ${id}`;
document.querySelector("#host").append(this.root);
window.previewState.records.set(id, this);
}
close() {
this.root.remove();
if (document.querySelector("#repair").checked) {
window.previewState.records.delete(this.id);
}
}
}
document.querySelector("#cycle").addEventListener("click", () => {
for (let i = 0; i < 10; i++) {
new DetachedPreview(++window.previewState.nextId).close();
}
document.querySelector("#status").textContent =
`Retained records: ${window.previewState.records.size}`;
});
</script>
</html>
The Map is application state, not a history feature. A record is meant to exist only while its preview is open. The checkbox lets you compare broken and repaired cleanup without changing any other behavior.
Record growth across the same action
- Reload the file. Do not inspect a preview element or evaluate the records in the Console.
- Open DevTools, select Memory, choose Heap snapshot, and take a baseline snapshot.
- Click Open and close 10 previews once. The page should say
Retained records: 10. Take another snapshot. - Click once more, confirm 20 retained records, and take a third snapshot.
- Select the second snapshot, switch Summary to Comparison, and choose the baseline. Filter for
DetachedPreview. Repeat with the third snapshot against the baseline.
For this fixture, expect ten extra DetachedPreview instances after one click and twenty after two. Constructor functions and prototypes are separate entries; inspect the actual instances. The page counter is a useful cross-check, but the snapshot tells you what those records retain. Heap byte totals and internal browser labels vary, so do not use a particular megabyte value as the pass condition.
Follow the path to the detached node
Expand a retained DetachedPreview instance and inspect its root property. It points to an HTMLSectionElement that is no longer attached to the document. You can also filter for Detached and find the section. Use the Retainers pane to work back toward a root:
Window
previewState
records (Map)
map entry value: DetachedPreview
root: detached HTMLSectionElement
This is the logical ownership path, not a promise that every DevTools row has exactly this label. The Map may appear through an internal table. Expand the entries until you can connect the section to the preview record and the record to previewState.records. Removing the section only disconnected it from the document. The registry is still reachable from window, so its values remain reachable too.
Do not log whole objects, use “Store as global variable,” or leave a paused debugger holding a preview while measuring. DevTools can retain objects you inspect. If the path ends in the debugger or Console rather than the registry, clear that inspection state and start from a fresh reload.
Repair the owner and repeat from zero
Reload first, then enable Delete the record on close. Take a fresh baseline and repeat the two clicks and snapshots. Expect the counter to stay at zero, with no surviving demo instances after collection. In production the cleanup would be unconditional:
close() {
this.root.remove();
window.previewState.records.delete(this.id);
}
Turning on the checkbox after the broken run does not remove the twenty old entries. It only repairs future closes. That is why a fresh baseline matters. If a real application needs closed-preview history, store identifiers or serializable metadata separately rather than the view object and its DOM tree.
When the result does not match
- The counter is zero but the snapshot retains a node: follow its retainers. Another cache, a debugger handle, or another owner may still reference it.
- Only total heap bytes grow: repeat the same lifecycle and compare the demo instance count. Compilation and one-time browser work are not evidence that this registry leaks.
- The fixed run still has twenty old records: reload before taking its baseline; the checkbox does not repair existing entries.
- A detached node exists temporarily: detachment by itself is not a bug. The problem here is unused records accumulating across completed lifecycles.
Try a second owner
Before closing each preview, imagine also adding its root to a long-lived savedNodes array. Would deleting the Map entry now release the section?
Answer: no. The array would provide another strong reference. Remove that entry too, or store the preview ID instead. Confirm the second retaining path disappears in a new snapshot; one successful deletion does not prove that all owners were cleaned up.
Reference: Chrome DevTools heap snapshots.
About
Trace a detached preview from its DOM node to an application Map, repair cleanup, and verify repeated open-and-close cycles with heap snapshots.
- Responsible
- Christopher R
- Last Update
- 09/03/2026
- Reading Time
- 6 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.