JS
() =>
JavaScript · Chrome DevTools · Performance · Memory
JavaScript Memory Leaks mit Chrome DevTools
systematisch aufspüren und dauerhaft beheben

Memory Leaks in JavaScript-Anwendungen sind unsichtbar – bis der Browser abstürzt oder die Seite nach Minuten spürbar langsamer wird. Heap Snapshots, Allocation Timelines und das Retainer-Diagramm in Chrome DevTools machen unsichtbare Speicherlecks sichtbar, lokalisierbar und behebbar.

15 Min. Lesezeit Heap Snapshot · Allocation Timeline · Retainer · detached DOM Chrome 120+ · JavaScript ES2024

1. Wie JavaScript-Speicherverwaltung wirklich funktioniert

JavaScript-Engines wie V8 verwalten Speicher automatisch über einen Garbage Collector. Der GC durchläuft den sogenannten Object Graph – alle Objekte, die über eine Kette von Referenzen vom globalen Scope oder aktiven Call-Stack erreichbar sind. Objekte, die nicht mehr erreichbar sind, gelten als "garbage" und werden freigegeben. Ein Memory Leak entsteht genau dann, wenn Objekte erreichbar bleiben, obwohl sie aus Programmsicht nicht mehr benötigt werden. Der GC kann sie nicht freigeben, weil eine Referenz – oft eine unbeabsichtigte – das Objekt im Object Graph hält.

V8 verwendet einen generationalen GC: Kurzlebige Objekte im "New Space" werden häufig gesammelt (Minor GC), langlebige Objekte wandern in den "Old Space" und werden seltener gesammelt (Major GC / Mark-Compact). Memory Leaks äußern sich typischerweise als kontinuierliches Wachstum des Old Space über Zeit, das sich auch nach Major-GC-Zyklen nicht vollständig reduziert. Dieses Muster ist das erste diagnostische Signal: Wenn die Heap-Kurve in Chrome DevTools nach jedem GC-Zyklus etwas höher landet als beim vorherigen Zyklus, liegt sehr wahrscheinlich ein Memory Leak vor.

2. Die häufigsten Ursachen von Memory Leaks

Vier Muster verursachen den Großteil aller JavaScript Memory Leaks in der Praxis. Erstens: Event-Listener, die auf DOM-Knoten registriert werden, die später aus dem DOM entfernt werden, ohne dass der Listener vorher mit removeEventListener deregistriert wird. Der DOM-Knoten bleibt im Speicher, weil der Event-Listener eine Referenz hält – das nennt man einen "detached DOM node". Zweitens: Closures, die unbeabsichtigt Variablen aus dem umgebenden Scope festhalten. Wenn eine Closure in einem langlebigen Objekt gespeichert wird, bleiben alle im Closure sichtbaren Variablen im Speicher.

Drittens: Timer und Intervalle – ein setInterval-Callback, der nie mit clearInterval gestoppt wird, hält alle im Callback referenzierten Objekte für die Laufzeit der Seite am Leben. Viertens: globale Variablen und Caches ohne Größenbeschränkung. Eine Map oder ein Array, das kontinuierlich mit neuen Einträgen befüllt wird, ohne dass alte Einträge entfernt werden, wächst unbegrenzt. Diese vier Muster sind die Ziele der Chrome DevTools Memory-Analyse – wer sie kennt, weiß, worauf er beim Untersuchen eines Memory Leaks achten muss.

3. Chrome DevTools Memory-Panel im Überblick

Das Memory-Panel in Chrome DevTools (F12 → Memory) bietet drei Profiling-Modi: "Heap Snapshot" erstellt eine Momentaufnahme aller Objekte im Heap zu einem bestimmten Zeitpunkt. "Allocation on timeline" zeichnet kontinuierlich auf, wann welche Objekte allokiert werden und ob sie den nächsten GC-Zyklus überleben. "Allocation sampling" ist ein niedrig-invasives Profiling, das mit statistischem Sampling den Speicherverbrauch nach Call-Stack aufschlüsselt. Für die Diagnose von Memory Leaks sind Heap Snapshots und die Allocation Timeline die wichtigsten Werkzeuge.

Vor jeder Memory-Analyse sollte man im Performance-Panel zunächst beobachten, ob der Heap über Zeit wächst. Dazu "Record" starten, typische Benutzerinteraktionen durchführen, dann "Collect garbage" (Mülltonnen-Symbol) klicken und das Heap-Wachstum beobachten. Ein nach dem GC-Klick sofort weiter wachsender Heap ist das klarste Signal für einen aktiven Memory Leak. Erst dann lohnt sich der tiefere Einstieg in Heap Snapshots.

4. Heap Snapshots: Die Drei-Snapshot-Technik

Die zuverlässigste Methode zum Auffinden von Memory Leaks mit Heap Snapshots ist die Drei-Snapshot-Technik. Snapshot 1: Baseline nach dem Laden der Seite. Dann die verdächtige Aktion einmal ausführen (z.B. einen Dialog öffnen und schließen). Snapshot 2: Zustand nach der Aktion. Die Aktion nochmals ausführen. Snapshot 3: Zweiter Durchlauf. In Snapshot 3 dann in der Dropdown-Ansicht "Objects allocated between snapshot 1 and snapshot 2" wählen. Alles, was dort noch sichtbar ist, wurde in Schritt 1 allokiert und nicht freigegeben – ein klares Zeichen für einen Memory Leak.

In der Snapshot-Ansicht zeigt die "Retainers"-Spalte, warum ein Objekt nicht freigegeben wurde: welche Kette von Referenzen es im Heap hält. Typisch ist eine Kette wie: Closure → Funktion → Event-Listener → DOM-Knoten. Das Retainer-Diagramm liest man von unten nach oben: Das unterste Element ist der "GC root" (z.B. das Window-Objekt), das oberste Element ist das lecke Objekt. Über diesen Pfad kann man exakt nachvollziehen, welche Referenz den Memory Leak verursacht.


// DevTools Memory Profiling — Heap Snapshot helper (paste in DevTools Console)

// Step 1: Baseline — run this, take Snapshot 1 in DevTools
console.log("Heap baseline ready — take Snapshot 1");

// Step 2: Simulate the leaky action
const leakyStore = [];
function createLeak() {
  const bigData = new Array(100_000).fill("leak data"); // 100k strings
  const handler = () => console.log(bigData.length);    // closure holds bigData
  document.body.addEventListener("click", handler);     // no cleanup stored
  // handler is never removed — bigData stays alive as long as DOM does
  leakyStore.push(handler); // additional strong reference
}
createLeak();
console.log("Action done — take Snapshot 2");

// Step 3: Repeat action, then take Snapshot 3
// In Snapshot 3: switch dropdown to "Objects allocated between Snapshot 1 and 2"
// Look for Array(100000) entries — those are the leaked bigData arrays
// Check Retainers panel: closure → handler → EventListener → document.body

// Correct fix: store handler reference and remove it when done
let cleanHandler = null;
function createClean() {
  const data = new Array(100_000).fill("clean data");
  cleanHandler = () => console.log(data.length);
  document.body.addEventListener("click", cleanHandler);
}
function destroyClean() {
  document.body.removeEventListener("click", cleanHandler);
  cleanHandler = null; // release reference — GC can now collect data
}

5. Allocation Timeline für kontinuierliche Leaks

Während Heap Snapshots Momentaufnahmen liefern, eignet sich die Allocation Timeline für Memory Leaks, die sich über Zeit aufbauen – zum Beispiel durch ein setInterval, das in jeder Iteration neue Objekte erzeugt, oder durch einen WebSocket-Handler, der Nachrichten in einem wachsenden Array speichert. Im Memory-Panel "Allocation instrumentation on timeline" wählen, Recording starten, die Anwendung für 30–60 Sekunden normal bedienen, dann Recording stoppen. Die Ansicht zeigt blaue und graue Balken: Blaue Balken sind Allokationen, die zum Stopp-Zeitpunkt noch im Heap sind – potenzielle Leaks. Graue Balken sind bereits gesammelte Allokationen.

Auf einen blauen Balken klicken filtert die Objekte, die in diesem Zeitfenster allokiert wurden und noch leben. Der Call-Stack zum Allokationszeitpunkt wird angezeigt – das ist der direkteste Weg, den Code zu finden, der den Memory Leak verursacht. Bei einem setInterval-Leak sieht man regelmäßig wiederkehrende blaue Balken mit identischem Call-Stack, die nie grau werden. Das ist das visuelle Fingerabdruckmuster eines Timer-basierten Memory Leaks.

6. Detached DOM-Knoten aufspüren

Detached DOM-Knoten sind DOM-Elemente, die aus dem Dokumentbaum entfernt wurden, aber noch durch JavaScript-Referenzen im Speicher gehalten werden. Sie verursachen einen der häufigsten Memory Leaks in Single-Page-Applications, weil Frameworks wie React oder Vue intern DOM-Knoten erzeugen und entfernen, während JavaScript-Code Referenzen auf alte Knoten behält. In Chrome DevTools kann man direkt nach detached DOM-Knoten suchen: Heap Snapshot erstellen, im Suchfeld oben "Detached" eingeben. Alle Einträge der Klasse "Detached HTMLElement", "Detached HTMLDivElement" usw. sind Kandidaten für Memory Leaks.

Für jeden gefundenen detached Knoten zeigt das Retainer-Panel, welche JavaScript-Variable die Referenz hält. Häufig ist es eine globale Variable, ein Closure in einem Event-Handler oder eine Datenstruktur wie eine Map oder ein Set. Die Lösung ist immer dieselbe: Beim Entfernen eines DOM-Knotens alle Referenzen auf ihn und seine Kind-Elemente explizit auf null setzen. In modernen Codebasen hilft WeakRef für Caches, die DOM-Knoten referenzieren – der GC kann WeakRef-Ziele einsammeln, ohne dass der Code explizit aufräumen muss.

7. Event-Listener-Leaks diagnostizieren

Event-Listener sind die häufigste einzelne Ursache von Memory Leaks in JavaScript-Anwendungen. Jeder mit addEventListener registrierte Listener hält eine Referenz auf die Callback-Funktion und alle Variablen, die die Callback-Closure einfängt. Wird der Listener nie deregistriert, bleibt dieser gesamte Objektgraph am Leben. Chrome DevTools bietet seit Chrome 90 eine direkte Möglichkeit, Event-Listener zu inspizieren: Im Elements-Panel unter "Event Listeners" kann man für jeden DOM-Knoten sehen, welche Listener registriert sind und aus welchem Code-Bereich sie stammen.

Für eine systematische Analyse empfiehlt sich der Einsatz von getEventListeners(element) in der DevTools-Konsole – diese interne Chrome-API gibt ein Objekt zurück, das für jeden Event-Typ die registrierten Listener auflistet. Bei einer SPA, die Komponenten dynamisch mountet und unmountet, sollte man nach jedem unmount-Zyklus prüfen, ob der alte Knoten noch Listener hat. Das AbortController-Muster ist die modernste Lösung: Ein Controller wird erzeugt, sein Signal an alle addEventListener-Aufrufe übergeben, und beim Cleanup reicht ein einziges controller.abort(), um alle Listener gleichzeitig zu entfernen.


// Event listener leak patterns and fixes

// WRONG: listener added on each render, never removed
class ComponentLeak {
  mount() {
    // Each call to mount() adds another listener — none are ever removed
    document.addEventListener("keydown", (e) => {
      if (e.key === "Escape") this.close(); // closure holds `this`
    });
  }
  close() { /* ... */ }
}

// RIGHT: AbortController pattern — single abort() removes all listeners
class ComponentClean {
  #controller = null;

  mount() {
    this.#controller = new AbortController();
    const { signal } = this.#controller;

    // All listeners share the same signal — one abort() removes all
    document.addEventListener("keydown", (e) => {
      if (e.key === "Escape") this.close();
    }, { signal });

    window.addEventListener("resize", () => this.onResize(), { signal });
    document.addEventListener("click", (e) => this.onOutsideClick(e), { signal });
  }

  unmount() {
    this.#controller?.abort(); // removes all registered listeners at once
    this.#controller = null;
  }

  close() { this.unmount(); }
  onResize() { /* ... */ }
  onOutsideClick(e) { /* ... */ }
}

// Inspect listeners in DevTools console (Chrome only):
// getEventListeners(document)  → { keydown: [...], click: [...] }
// getEventListeners(document).keydown.length  → count of keydown listeners

8. Konkrete Fix-Strategien mit Code

Neben dem AbortController-Muster für Event-Listener gibt es vier weitere Fix-Strategien, die den Großteil der Memory Leaks in der Praxis abdecken. Für Timer-Leaks: Immer den Rückgabewert von setInterval und setTimeout speichern und beim Cleanup mit clearInterval bzw. clearTimeout stoppen. Bei React-Komponenten gehört das in den Cleanup-Return der useEffect-Funktion. Für Cache-Leaks: WeakMap statt Map verwenden, wenn DOM-Knoten oder Objekte als Schlüssel dienen – WeakMap-Einträge werden automatisch gelöscht, wenn der Schlüssel keine anderen Referenten mehr hat.

Für Observer-Leaks (IntersectionObserver, MutationObserver, ResizeObserver): Immer observer.disconnect() aufrufen, wenn der Observer nicht mehr benötigt wird. Observer halten implizite Referenzen auf die beobachteten Elemente und den Callback. Für Closure-Leaks in langlebigen Objekten: Variablen, die nur temporär benötigt werden, explizit auf null setzen, wenn sie nicht mehr gebraucht werden – das gibt dem GC das Signal, den referenzierten Objektgraph freizugeben. Diese Fixes sind einfach, aber die Konsequenz ihrer Anwendung macht den Unterschied zwischen einer stabilen und einer leckenden JavaScript-Anwendung.

9. Leak-Muster im direkten Vergleich

Die folgende Übersicht zeigt die häufigsten Memory Leak-Muster in JavaScript mit dem jeweiligen Fix und der Erkennungsmethode in Chrome DevTools.

Leak-Typ Ursache Fix DevTools-Signal
Event-Listener addEventListener ohne removeEventListener AbortController + abort() Detached DOM, wachsende Listener-Zahl
Timer setInterval ohne clearInterval ID speichern, clearInterval im Cleanup Regelmäßige blaue Balken in Timeline
Closure Große Variable in langlebiger Closure Variable nach Nutzung auf null setzen Große Objekte im Retainer-Pfad
Cache / Map Map wächst unbegrenzt WeakMap oder LRU-Cache mit Limit Stetig wachsende Map-Instanz im Heap
Observer MutationObserver ohne disconnect() observer.disconnect() im Teardown MutationObserver im Retainer-Pfad

// WeakMap cache — entries are automatically GC'd when key has no other references
const cache = new WeakMap();

function processElement(el) {
  if (cache.has(el)) return cache.get(el);  // cache hit — no recompute

  const result = expensiveComputation(el);
  cache.set(el, result); // key is el (DOM node) — GC-safe
  return result;
  // When el is removed from DOM and all JS refs drop, WeakMap entry is freed
}

// Timer cleanup in plain JS class
class PollingService {
  #intervalId = null;

  start(callback, ms = 5000) {
    this.#intervalId = setInterval(callback, ms);
  }

  stop() {
    clearInterval(this.#intervalId); // must always be called on teardown
    this.#intervalId = null;
  }
}

// Timer cleanup in React useEffect
// useEffect(() => {
//   const id = setInterval(fetchData, 5000);
//   return () => clearInterval(id); // React calls this on unmount
// }, []);

Mironsoft

JavaScript Performance-Analyse und Memory-Optimierung

JavaScript-Anwendung mit Memory-Problemen?

Wir analysieren JavaScript Memory Leaks mit Chrome DevTools, identifizieren die Ursachen im Heap und beheben sie dauerhaft – inklusive Monitoring-Strategie für die Produktion.

Heap-Analyse

Drei-Snapshot-Technik und Allocation Timeline zur Leak-Lokalisierung

Code-Fixes

AbortController, WeakMap, Observer-Teardown und Timer-Cleanup

Monitoring

Performance-Budget und Heap-Metriken in CI-Pipeline integrieren

10. Zusammenfassung

JavaScript Memory Leaks entstehen nicht durch Bugs in der Engine, sondern durch unbeabsichtigte Referenzen, die der Garbage Collector nicht durchbrechen kann. Die systematische Analyse mit Chrome DevTools – Drei-Snapshot-Technik, Allocation Timeline und Retainer-Diagramm – macht diese unsichtbaren Referenzen sichtbar. Die wichtigsten Ursachen sind Event-Listener ohne Deregistrierung, Timer ohne Cleanup, Closures, die große Objekte festhalten, und Caches ohne Größenbeschränkung. Für jeden dieser Leak-Typen gibt es ein klares Muster: AbortController, WeakMap, explizites null-Setzen und Observer-disconnect.

Der entscheidende Faktor ist nicht nur das Beheben gefundener Memory Leaks, sondern die Prävention durch konsistente Teardown-Disziplin: Jedes addEventListener braucht einen definierten Zeitpunkt für removeEventListener, jedes setInterval braucht ein clearInterval, jeder Observer braucht ein disconnect(). Diese Konvention in den Entwicklungsstandards eines Teams zu verankern – unterstützt durch automatisierte Memory-Tests in der CI-Pipeline – ist das effektivste Mittel gegen langfristige Memory-Probleme in JavaScript-Anwendungen.

JavaScript Memory Leaks — Das Wichtigste auf einen Blick

Diagnostik

Drei-Snapshot-Technik in Chrome DevTools: Baseline → Aktion → Vergleich. Allocation Timeline für kontinuierliche Leaks. Detached-DOM-Suche im Heap Snapshot.

Event-Listener

AbortController-Muster: Signal an alle addEventListener übergeben, einmalig abort() im Teardown aufrufen. Eliminiert die häufigste Leak-Quelle.

Caches & Maps

WeakMap für DOM-Knoten als Schlüssel – GC-safe. LRU-Cache mit fixer Größe für alle anderen Caches. Niemals unbegrenzt wachsende Maps.

Timer & Observer

setInterval-ID speichern und clearInterval im Cleanup aufrufen. MutationObserver, IntersectionObserver und ResizeObserver immer disconnect().

11. FAQ: JavaScript Memory Leaks mit Chrome DevTools

1Was ist ein JavaScript Memory Leak?
Objekte bleiben im Heap, weil eine unbeabsichtigte Referenz den GC blockiert. Der Speicherverbrauch wächst kontinuierlich, ohne sich nach GC-Zyklen zu erholen.
2Wie erkenne ich einen Memory Leak in Chrome DevTools?
Performance-Panel: Heap-Linie nach GC-Trigger beobachten. Memory-Panel: Drei-Snapshot-Technik oder Allocation Timeline – blaue Balken, die nie grau werden.
3Was ist die Drei-Snapshot-Technik?
Baseline → Aktion → Snapshot 2 → Aktion → Snapshot 3. In Snapshot 3 Filter "Objects allocated between Snapshot 1 and 2" – dort sichtbare Objekte sind Leak-Kandidaten.
4Was sind detached DOM-Knoten?
DOM-Elemente, aus dem Baum entfernt, aber durch JS-Referenzen noch im Heap. Im Heap Snapshot über die Suche "Detached" auffindbar.
5Wie behebe ich Event-Listener-Leaks?
AbortController-Muster: Signal an alle addEventListener, einmalig controller.abort() im Teardown. Entfernt alle Listener gleichzeitig.
6WeakMap statt Map?
Wenn DOM-Knoten oder Objekte als Schlüssel dienen. WeakMap-Einträge werden automatisch gelöscht, wenn der Schlüssel keine anderen starken Referenzen mehr hat.
7Timer-Leaks in der Timeline erkennen?
Regelmäßig wiederkehrende blaue Balken mit identischem Call-Stack in der Allocation Timeline, die nie grau werden – das Fingerabdruckmuster eines Timer-Leaks.
8Was zeigt das Retainer-Diagramm?
Die Referenzkette vom GC root zum lecken Objekt. Lesen von unten (window) nach oben (das Leak). Zeigt exakt, welche Referenz freigegeben werden muss.
9Observer-Leaks vermeiden?
Immer observer.disconnect() aufrufen – bei MutationObserver, IntersectionObserver und ResizeObserver. Observer halten implizit Referenzen auf Elemente und Callback.
10Memory Leaks automatisiert in CI testen?
Ja – mit Puppeteer/Playwright und dem Chrome DevTools Protocol (CDP): HeapProfiler.takeHeapSnapshot vor/nach Aktionen vergleichen. Heap-Wachstum als CI-Metrik definieren.