Retainer, Detached Nodes und Allocation Timelines lesen
Ein einzelner Heap Snapshot zeigt nur eine Momentaufnahme, aber die Drei Snapshot Technik entlarvt systematisch, welche Objekte trotz Garbage Collector im Speicher bleiben. Retainer Pfade zeigen exakt, welche Referenz eine Freigabe verhindert, und machen aus vagem Speicherverdacht eine konkrete Ursache im Code.
Inhaltsverzeichnis
- 1. Warum ein einzelner Heap Snapshot nicht ausreicht
- 2. Aufbau eines Heap Snapshots: Objekte, Kanten und Größen
- 3. Retainer Pfade lesen: warum ein Objekt nicht freigegeben wird
- 4. Die Drei Snapshot Technik im Detail
- 5. Detached DOM Nodes finden und verstehen
- 6. Closures und Event Listener als häufige Leak Quelle
- 7. Allocation Timeline: Zuweisungen über die Zeit verfolgen
- 8. Praktischer Workflow für reproduzierbare Analysen
- 9. Memory Panel Werkzeuge im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein einzelner Heap Snapshot nicht ausreicht
Ein Heap Snapshot ist eine vollständige Momentaufnahme aller Objekte im JavaScript Heap zu einem bestimmten Zeitpunkt, inklusive ihrer Größe und ihrer Beziehungen zueinander. Ein einzelner Heap Snapshot zeigt zwar, welche Objekte gerade existieren und wie viel Speicher sie belegen, verrät aber nichts darüber, ob diese Objekte tatsächlich ein Speicherleck darstellen oder einfach nur normaler, aktiver Anwendungszustand sind. Ohne einen Vergleichspunkt ist ein einzelner Heap Snapshot für die Leck Diagnose kaum aussagekräftig.
Ein Speicherleck in JavaScript entsteht immer dann, wenn ein Objekt, das eigentlich nicht mehr gebraucht wird, weiterhin von einer aktiven Referenzkette erreichbar bleibt und deshalb vom Garbage Collector nicht eingesammelt werden kann. Ein Heap Snapshot allein zeigt diese Referenzketten zwar an, aber erst der Vergleich zwischen mehreren Heap Snapshots über die Zeit macht sichtbar, welche Objekte sich kontinuierlich ansammeln, statt nach Gebrauch wieder freigegeben zu werden.
Die Praxis zeigt, dass isolierte Heap Snapshots häufig zu Fehlinterpretationen führen: ein großer Heap Snapshot kann schlicht bedeuten, dass die Anwendung gerade viele Daten aktiv verarbeitet, ohne dass ein einziges Objekt tatsächlich geleakt ist. Erst die systematische Methode mit mehreren Heap Snapshots, insbesondere die im nächsten Abschnitt vorgestellte Drei Snapshot Technik, trennt normales Speicherwachstum zuverlässig von echten Memory Leaks.
2. Aufbau eines Heap Snapshots: Objekte, Kanten und Größen
Technisch besteht ein Heap Snapshot aus einem gerichteten Graphen: Knoten repräsentieren Objekte im Heap, Kanten repräsentieren Referenzen zwischen diesen Objekten. Jeder Knoten trägt Informationen über seinen Typ, etwa ob es sich um ein einfaches Objekt, ein Array, eine Closure oder einen DOM Node handelt, sowie über seine Shallow Size, den Speicher, den das Objekt selbst direkt belegt, ohne die Objekte, auf die es verweist.
Die Retained Size eines Objekts im Heap Snapshot ist die wichtigere Kennzahl für die Leck Analyse: sie gibt an, wie viel Speicher insgesamt freigegeben würde, wenn dieses eine Objekt aus dem Heap entfernt würde, inklusive aller Objekte, die ausschließlich über dieses eine Objekt erreichbar sind. Ein Objekt mit kleiner Shallow Size, aber riesiger Retained Size ist oft der eigentliche Wurzelpunkt eines Speicherproblems, weil es einen ganzen Baum von sonst nicht mehr erreichbaren Objekten am Leben hält.
Das Chrome DevTools Memory Panel bietet drei Ansichten auf einen Heap Snapshot: Summary gruppiert Objekte nach Konstruktor Namen und zeigt Gesamtgrößen pro Typ, Comparison vergleicht zwei Heap Snapshots und zeigt Deltas, und Containment zeigt die rohe Objektstruktur ausgehend von den Wurzelobjekten. Für die meisten Leck Analysen ist Comparison die wertvollste Ansicht, weil sie direkt zeigt, welche Objekttypen zwischen zwei Zeitpunkten zugenommen haben.
// Programmatically triggering a heap snapshot via Chrome DevTools Protocol
// Useful for automated memory regression tests in CI
import CDP from "chrome-remote-interface";
async function captureHeapSnapshot(port) {
const client = await CDP({ port });
const { HeapProfiler } = client;
await HeapProfiler.enable();
let snapshotData = "";
HeapProfiler.addHeapSnapshotChunk(({ chunk }) => {
snapshotData += chunk;
});
await HeapProfiler.takeHeapSnapshot({ reportProgress: false });
await client.close();
return snapshotData; // JSON heap snapshot, can be loaded into DevTools
}
3. Retainer Pfade lesen: warum ein Objekt nicht freigegeben wird
Der Retainer Pfad eines Objekts im Heap Snapshot zeigt die Kette von Referenzen, die dieses Objekt vor der Freigabe durch den Garbage Collector bewahrt, ausgehend von einem Wurzelobjekt wie window oder einem globalen Kontext. Im Memory Panel lässt sich dieser Pfad per Klick auf ein Objekt in der unteren Hälfte des Fensters aufklappen, mit jedem Glied der Kette als eigener, weiter aufklappbarer Eintrag.
Das Lesen von Retainer Pfaden folgt einem festen Muster: von unten nach oben zeigt der Pfad, welches konkrete Objekt, etwa eine Variable in einer Closure oder ein Eintrag in einem Array, die Referenz hält. Häufig findet sich am oberen Ende des Retainer Pfads ein global gültiger Event Listener, ein Timer, der nie gecleart wurde, oder eine Map beziehungsweise ein Set, dem Einträge hinzugefügt, aber nie wieder entfernt werden.
Ein wichtiger Hinweis beim Lesen von Retainer Pfaden im Heap Snapshot: die Bezeichnung in eckigen Klammern, etwa [[Scopes]] oder ein Property Name, zeigt die Art der Referenz. Ein Eintrag namens context deutet meist auf eine Closure hin, die eine äußere Variable festhält. Diese Unterscheidung hilft, schnell zwischen verschiedenen Leak Mustern zu unterscheiden, ohne den gesamten Code manuell durchsuchen zu müssen.
4. Die Drei Snapshot Technik im Detail
Die Drei Snapshot Technik ist die zuverlässigste Methode, um echte Memory Leaks von normalem Speicherverhalten zu unterscheiden. Der Ablauf: zunächst wird ein erster Heap Snapshot als Baseline erstellt, nachdem die Anwendung sich in einem stabilen Ruhezustand befindet. Danach wird die verdächtige Aktion, etwa das Öffnen und Schließen eines Modals oder eine Navigation zwischen zwei Routen, mehrfach wiederholt, gefolgt von einem zweiten Heap Snapshot.
Nach dem zweiten Heap Snapshot wird dieselbe Aktion erneut mehrfach wiederholt, und ein dritter Heap Snapshot wird erstellt. Der entscheidende Schritt ist nun der Vergleich: Objekte, die zwischen dem ersten und zweiten Heap Snapshot zunehmen und zwischen dem zweiten und dritten weiter kontinuierlich zunehmen, sind mit hoher Wahrscheinlichkeit ein echtes Speicherleck. Objekte, die nur einmal zunehmen und danach stabil bleiben, sind meist normales, erwartetes Speicherverhalten der Anwendung.
Diese Methode funktioniert deshalb so zuverlässig, weil ein tatsächliches Speicherleck sich per Definition mit jeder Wiederholung der auslösenden Aktion weiter aufsummiert, während einmaliges Speicherwachstum, etwa durch Caching oder Lazy Initialisierung, nach dem ersten Vorkommen konstant bleibt. Das Memory Panel bietet dafür direkt die Option, nur die Objekte anzuzeigen, die zwischen zwei ausgewählten Snapshots neu hinzugekommen sind, was die Drei Snapshot Technik erheblich vereinfacht.
// Automating the three-snapshot technique with Puppeteer
async function detectMemoryLeak(page, triggerAction, iterations = 5) {
const session = await page.target().createCDPSession();
await session.send("HeapProfiler.enable");
async function takeSnapshot() {
let data = "";
session.on("HeapProfiler.addHeapSnapshotChunk", (e) => (data += e.chunk));
await session.send("HeapProfiler.takeHeapSnapshot");
return data;
}
await triggerAction(page); // warm up, e.g. open/close a modal once
const baseline = await takeSnapshot();
for (let i = 0; i < iterations; i++) await triggerAction(page);
const second = await takeSnapshot();
for (let i = 0; i < iterations; i++) await triggerAction(page);
const third = await takeSnapshot();
// Compare object counts by constructor between second and third snapshot
// Objects growing consistently across both deltas indicate a real leak
return { baseline, second, third };
}
5. Detached DOM Nodes finden und verstehen
Ein Detached DOM Node ist ein DOM Element, das aus dem sichtbaren Dokumentbaum entfernt wurde, aber weiterhin von JavaScript Code referenziert wird und deshalb nicht vom Garbage Collector eingesammelt werden kann. Diese Situation entsteht typischerweise, wenn ein Event Listener oder eine Variable eine Referenz auf ein Element hält, das per removeChild oder durch Ersetzen des innerHTML entfernt wurde, ohne dass die Referenz selbst aufgeräumt wurde.
Im Heap Snapshot lassen sich Detached DOM Nodes gezielt über die Filterfunktion in der Summary Ansicht finden, indem nach Detached gefiltert wird. Das Memory Panel zeigt dann alle DOM Elemente, die zwar noch im Speicher existieren, aber keine Verbindung mehr zum aktiven Dokument haben. Diese Liste ist oft der schnellste Weg, um DOM bezogene Speicherlecks zu identifizieren, insbesondere in Single Page Applications, in denen Komponenten häufig gemountet und wieder entfernt werden.
Ein besonders tückisches Muster mit Detached DOM Nodes entsteht, wenn ein gesamter Detached Subtree an einem einzigen Element hängt, das selbst wiederum von einer Closure referenziert wird. In diesem Fall zeigt der Heap Snapshot möglicherweise nur ein einzelnes, klein wirkendes Detached Element, dessen Retained Size aber durch hunderte Kind Elemente enorm aufgebläht ist. Der Retainer Pfad dieses einen Wurzelelements führt dann direkt zur verursachenden Code Stelle.
6. Closures und Event Listener als häufige Leak Quelle
Closures sind eine der häufigsten Ursachen für Speicherlecks in JavaScript, weil sie per Definition Referenzen auf ihren umgebenden Gültigkeitsbereich festhalten, auch wenn nur eine einzige Variable aus diesem Bereich tatsächlich benötigt wird. Eine Funktion, die als Event Listener registriert wird und dabei versehentlich eine große Datenstruktur aus ihrem umgebenden Scope referenziert, hält diese Datenstruktur im Speicher, solange der Event Listener registriert bleibt, selbst wenn die Daten längst nicht mehr gebraucht werden.
Ein besonders häufiges Muster: ein Event Listener wird bei jeder Komponenten Instanziierung neu registriert, aber nie beim Entfernen der Komponente wieder entfernt. In Frameworks mit Lifecycle Hooks führt das dazu, dass sich mit jeder Erstellung und Zerstörung einer Komponente ein weiterer Event Listener ansammelt, jeder davon mit seiner eigenen Closure und den darin gefangenen Referenzen. Der Heap Snapshot zeigt dieses Muster als kontinuierlich wachsende Anzahl an Funktions Objekten desselben Namens.
Die zuverlässigste Gegenmaßnahme ist, jeden addEventListener Aufruf mit einem entsprechenden removeEventListener Aufruf im Cleanup Pfad zu koppeln, idealerweise mit einem AbortController, der mehrere Listener gebündelt entfernt. Für Timer gilt dasselbe Prinzip: jeder setInterval Aufruf braucht ein korrespondierendes clearInterval, sonst hält der Timer seine Closure und alle darin referenzierten Objekte dauerhaft am Leben.
7. Allocation Timeline: Zuweisungen über die Zeit verfolgen
Die Allocation Timeline im Memory Panel zeichnet Speicherzuweisungen über einen Zeitraum auf und zeigt sie als Balkendiagramm, in dem jeder blaue Balken eine Gruppe von Zuweisungen zu einem bestimmten Zeitpunkt repräsentiert. Im Gegensatz zu einem einzelnen Heap Snapshot, der nur den Endzustand zeigt, macht die Allocation Timeline sichtbar, wann genau im zeitlichen Verlauf Speicher zugewiesen wurde, was die Zuordnung zu einer bestimmten Nutzeraktion erheblich erleichtert.
Ein besonders nützliches Feature der Allocation Timeline ist die Möglichkeit, einen bestimmten Zeitbereich per Maus auszuwählen und dabei nur die Objekte anzuzeigen, die in genau diesem Bereich zugewiesen wurden und zum Ende der Aufnahme noch im Speicher vorhanden sind. Objekte, die zwar zugewiesen, aber vor Ende der Aufnahme bereits wieder freigegeben wurden, erscheinen im Diagramm als hellere, ausgegraute Balken, was normales Speicherverhalten von potenziell problematischem Verhalten optisch trennt.
Die Allocation Timeline eignet sich besonders für die Untersuchung wiederkehrender Nutzeraktionen wie Scrollen oder Filtern, bei denen unklar ist, ob jede Wiederholung neue Objekte anlegt, die nach Gebrauch korrekt freigegeben werden, oder ob sich mit jeder Wiederholung zusätzlicher Speicher ansammelt. Zusammen mit der Drei Snapshot Technik liefert die Allocation Timeline ein vollständiges Bild sowohl des Endzustands als auch des zeitlichen Verlaufs eines möglichen Speicherlecks.
8. Praktischer Workflow für reproduzierbare Analysen
Ein reproduzierbarer Workflow für Heap Snapshot Analysen beginnt immer mit einer manuellen Garbage Collection über den Papierkorb Button im Memory Panel, bevor überhaupt ein Snapshot erstellt wird. Ohne diesen Schritt können noch nicht eingesammelte, aber bereits nicht mehr referenzierte Objekte den Snapshot verfälschen und den Eindruck eines Lecks erwecken, wo tatsächlich nur der Garbage Collector noch nicht gelaufen ist.
Für konsistente Ergebnisse sollte die verdächtige Aktion immer über denselben Interaktionspfad ausgelöst werden, idealerweise automatisiert über ein Skript statt manuell per Maus, um menschliche Variation in Timing und Reihenfolge auszuschließen. Die Kombination aus programmatischer Steuerung über das Chrome DevTools Protocol und den drei Heap Snapshots der Drei Snapshot Technik liefert reproduzierbare, in CI Pipelines integrierbare Speicher Regressionstests, die neue Lecks erkennen, bevor sie in Produktion gelangen.
9. Memory Panel Werkzeuge im Vergleich
Das Memory Panel bietet mehrere Werkzeuge für unterschiedliche Fragestellungen. Die folgende Tabelle ordnet ein, welches Werkzeug für welche Art von Speicherproblem am besten geeignet ist.
| Werkzeug | Zeigt | Stärke | Grenze |
|---|---|---|---|
| Heap Snapshot | Vollständiger Objektgraph zu einem Zeitpunkt | Retainer Pfade, exakte Ursache | Nur Momentaufnahme, kein Verlauf |
| Drei Snapshot Technik | Delta über mehrere Wiederholungen | Trennt echte Lecks von normalem Wachstum | Erfordert mehrere manuelle Schritte |
| Allocation Timeline | Zuweisungen über die Zeit | Zeitliche Zuordnung zu Aktionen | Kein Retainer Pfad direkt im Diagramm |
| Allocation Sampling | Statistische Verteilung nach Funktion | Geringer Overhead, lange Aufnahmen | Keine Objekt Identität, nur Aggregate |
In der Praxis ergänzen sich diese Werkzeuge. Allocation Sampling eignet sich für einen ersten, langlaufenden Überblick mit geringem Overhead, die Allocation Timeline zeigt dann den zeitlichen Kontext eines auffälligen Bereichs, und die Drei Snapshot Technik mit anschließender Retainer Pfad Analyse liefert schließlich die exakte Codezeile, die für das Leck verantwortlich ist.
Mironsoft
JavaScript Speicherprofiling und Memory Leak Diagnose
Speicherlecks systematisch aufspüren?
Wir analysieren eure Anwendung mit Heap Snapshots und der Drei Snapshot Technik, finden Retainer Pfade und Detached DOM Nodes und liefern konkrete Fixes statt Vermutungen.
Leak Diagnose
Drei Snapshot Technik anwenden, Retainer Pfade bis zur Ursache verfolgen
Detached Nodes beheben
Event Listener und Closures bereinigen, AbortController einsetzen
CI Regression Tests
Automatisierte Heap Snapshot Vergleiche in Pipelines integrieren
10. Zusammenfassung
Ein einzelner Heap Snapshot zeigt nur eine Momentaufnahme und reicht für belastbare Leck Diagnose nicht aus. Die Drei Snapshot Technik trennt echte Speicherlecks zuverlässig von normalem, einmaligem Speicherwachstum, indem sie prüft, ob Objekte mit jeder Wiederholung einer Aktion kontinuierlich zunehmen. Retainer Pfade zeigen dann exakt, welche Referenzkette ein Objekt vor der Freigabe durch den Garbage Collector bewahrt, meist über Closures, vergessene Event Listener oder nicht gecancelte Timer.
Detached DOM Nodes lassen sich gezielt über die Filterfunktion im Memory Panel finden, und die Allocation Timeline ergänzt die Analyse um die zeitliche Dimension, die ein reiner Heap Snapshot nicht liefert. Wer diese Werkzeuge kombiniert und den Workflow mit manueller Garbage Collection vor jedem Snapshot konsistent hält, verwandelt vage Speicherverdachtsfälle in konkrete, reproduzierbare Diagnosen mit klarer Codezeile als Ursache.
Heap Snapshots und Speicherprofiling — Das Wichtigste auf einen Blick
Drei Snapshot Technik
Baseline, dann Aktion wiederholen, zweiter Snapshot, erneut wiederholen, dritter Snapshot vergleichen.
Retainer Pfade
Zeigen die Referenzkette vom Wurzelobjekt bis zum nicht freigegebenen Objekt, meist über Closures oder Listener.
Detached DOM Nodes
Über den Detached Filter in der Summary Ansicht gezielt auffindbar, oft ganze Subtrees an einem Wurzelelement.
Allocation Timeline
Zeigt Zuweisungen über die Zeit, ausgegraute Balken markieren bereits wieder freigegebene Objekte.