Vue Memory Leaks aufspüren und beheben
AI generated
<v/>
{ }
Vue 3 · Performance · Memory Leaks · Devtools
Vue Memory Leaks
aufspüren und beheben, bevor der Tab abstürzt

Vue Memory Leaks entstehen fast immer an denselben drei Stellen: vergessene Event-Listener, nicht gestoppte Timer und Watcher ohne Cleanup beim Unmounten einer Komponente. Mit Chrome-Devtools-Heap-Snapshots lässt sich jeder Verdacht belastbar bestätigen, statt auf Basis von Vermutungen Code zu ändern, der das eigentliche Leck gar nicht betrifft.

20 Min. Lesezeit onUnmounted · Heap Snapshot · Event Listener · Timer Vue 3.x · Chrome Devtools

1. Warum Memory Leaks in Single-Page-Apps besonders schmerzhaft sind

Vue Memory Leaks treffen Single-Page-Anwendungen härter als klassische Mehrseiten-Webseiten, weil ein voller Seiten-Reload bei Letzteren regelmäßig den gesamten JavaScript-Speicher zurücksetzt. In einer Vue-SPA dagegen bleibt der Browser-Tab über Stunden geöffnet, während Nutzer zwischen Routen navigieren. Jede Komponente, die beim Verlassen nicht sauber aufräumt, hinterlässt Reste im Speicher, die sich über viele Navigationen hinweg akkumulieren, bis der Tab spürbar langsamer wird oder abstürzt.

Das Tückische an Vue Memory Leaks: Sie zeigen sich selten sofort. Ein einzelner vergessener Event-Listener fällt in der Entwicklung kaum auf, weil der Effekt erst nach Dutzenden Navigationen messbar wird. Genau das macht sie in der Praxis so gefährlich, weil sie oft erst bei Nutzern auffallen, die eine Anwendung über einen ganzen Arbeitstag geöffnet lassen, nicht bei Entwicklern, die im Dev-Modus alle paar Minuten neu laden. Die folgenden Abschnitte zeigen die häufigsten Ursachen und ein methodisches Vorgehen, um sie zu bestätigen.

2. Vergessene Event-Listener als häufigste Ursache

Der mit Abstand häufigste Auslöser für Vue Memory Leaks ist ein addEventListener()-Aufruf auf window, document oder einem anderen langlebigen Objekt, dem kein passendes removeEventListener() beim Unmounten der Komponente gegenübersteht. Solange der Listener registriert bleibt, hält der Browser eine Referenz auf die Callback-Funktion, und über Closures häufig auch auf die gesamte Komponenteninstanz und deren reaktiven Zustand. Die Komponente kann dann nicht vom Garbage Collector eingesammelt werden, selbst wenn sie im Vue-Komponentenbaum längst nicht mehr existiert.

Die zuverlässige Lösung folgt einem festen Muster: Jeder addEventListener()-Aufruf in onMounted() bekommt ein exakt spiegelbildliches removeEventListener() in onUnmounted(), mit identischer Funktionsreferenz. Ein häufiger Fehler dabei: Eine Inline-Arrow-Function wird bei addEventListener übergeben, aber beim Entfernen wird eine neue Arrow-Function mit demselben Code, aber anderer Referenz übergeben. Da removeEventListener() auf exakte Funktionsreferenz-Gleichheit prüft, bleibt der ursprüngliche Listener in diesem Fall aktiv, obwohl der Code so aussieht, als würde er korrekt aufgeräumt.


// WRONG: new arrow function on cleanup does not match the one added
export default {
  mounted() {
    window.addEventListener('resize', () => this.handleResize())
  },
  beforeUnmount() {
    // this removes nothing - different function reference than the one added
    window.removeEventListener('resize', () => this.handleResize())
  },
}

// RIGHT: store a stable reference to the exact same function
import { onMounted, onUnmounted } from 'vue'

function handleResize() {
  console.log('window resized')
}

onMounted(() => {
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  // same function reference - actually removes the listener
  window.removeEventListener('resize', handleResize)
})

3. Offene Timer und Intervals

Nicht gestoppte setInterval()-Aufrufe sind die zweithäufigste Quelle für Vue Memory Leaks, mit einem entscheidenden Unterschied zu Event-Listenern: Ein laufendes Interval hält die Komponenteninstanz nicht nur im Speicher, sondern führt aktiv weiterhin Code aus, selbst nachdem die Komponente aus dem DOM entfernt wurde. Das kann zu Fehlern führen, wenn der Interval-Callback versucht, auf ein nicht mehr existierendes DOM-Element zuzugreifen, oder still im Hintergrund weiterläuft und API-Anfragen für eine bereits verlassene Ansicht sendet.

Das Cleanup-Pattern ist strukturell identisch zu Event-Listenern: Die von setInterval() zurückgegebene ID wird in einer Variable gespeichert und beim Unmounten mit clearInterval() gestoppt. Bei komplexeren Komponenten mit mehreren Timern hilft ein Array, alle IDs zu sammeln und in einer einzigen Cleanup-Funktion durchzugehen, statt für jeden Timer eine eigene Variable zu pflegen und beim Aufräumen leicht einen zu vergessen.


import { ref, onMounted, onUnmounted } from 'vue'

const elapsedSeconds = ref(0)
let intervalId = null

onMounted(() => {
  intervalId = setInterval(() => {
    elapsedSeconds.value++
  }, 1000)
})

onUnmounted(() => {
  // without this line, the interval keeps running after the component is gone
  clearInterval(intervalId)
})

// Multiple timers: collect IDs in an array, clear all in one loop
const timerIds = []

function startPolling(url, callback, ms) {
  const id = setInterval(() => callback(url), ms)
  timerIds.push(id)
  return id
}

onUnmounted(() => {
  timerIds.forEach((id) => clearInterval(id))
})

4. Watcher und Subscriptions ohne Cleanup

Innerhalb einer Vue-Komponente erstellte watch()- und watchEffect()-Aufrufe werden von Vue automatisch gestoppt, sobald die Komponente unmounted wird, das ist kein typischer Auslöser für Vue Memory Leaks. Kritisch wird es erst, wenn ein Watcher außerhalb des Komponenten-Setup-Kontexts erzeugt wird, etwa in einer globalen Store-Initialisierung, oder wenn eine externe Subscription, zum Beispiel ein WebSocket oder ein RxJS-Observable, manuell abonniert wird, ohne dass Vue davon weiß.

Für externe Subscriptions gilt dieselbe Regel wie für Event-Listener: Jedes subscribe() braucht ein gespiegeltes unsubscribe() in onUnmounted(). Ein besonders subtiler Fall von Vue Memory Leaks entsteht, wenn ein Composable einen Watcher auf eine global geteilte reaktive Referenz registriert und diese Referenz dabei versehentlich eine Referenz auf die aufrufende Komponente in ihrer Callback-Closure einschließt. Die globale Referenz überlebt naturgemäß die Komponente, und mit ihr die Closure samt referenzierter Komponenteninstanz.


import { onMounted, onUnmounted } from 'vue'
import { globalWebSocket } from '@/services/websocket'

// External subscription outside Vue's own reactivity tracking
let unsubscribe = null

onMounted(() => {
  unsubscribe = globalWebSocket.subscribe('price-update', (data) => {
    // if this callback captures component state via closure,
    // the subscription keeps the whole component instance alive
    console.log('Price update received', data)
  })
})

onUnmounted(() => {
  // without this call, globalWebSocket keeps a reference to this closure forever
  if (unsubscribe) unsubscribe()
})

5. Closures und versehentlich festgehaltene Referenzen

Ein subtilerer Auslöser für Vue Memory Leaks entsteht durch JavaScript-Closures, die mehr Kontext festhalten, als eigentlich nötig wäre. Eine Callback-Funktion, die tief in einem Composable oder einer Utility-Bibliothek gespeichert wird, hält implizit eine Referenz auf alle Variablen ihres umschließenden Scopes, auch wenn die Funktion selbst nur eine einzige davon tatsächlich benötigt. Wird diese Callback-Funktion langfristig gespeichert, etwa in einem globalen Cache oder einer Registry, bleibt der gesamte umschließende Scope samt Komponenteninstanz im Speicher, obwohl nur ein kleiner Teil davon gebraucht wird.

Die Gegenmaßnahme: Callback-Funktionen, die langfristig gespeichert werden, sollten nur die konkret benötigten, primitiven Werte referenzieren, statt ganze Objekte oder this aus dem Options-API-Kontext. In der Composition API hilft es, Werte explizit zu destrukturieren und diese destrukturierten Primitives in der Callback zu verwenden, statt die reaktiven Objekte selbst in der Closure zu belassen, wo sie mehr referenzieren als nötig.

6. Heap-Snapshots in Chrome Devtools aufnehmen

Ein Verdacht auf Vue Memory Leaks lässt sich zuverlässig bestätigen, indem man in den Chrome Devtools im Memory-Tab zwei Heap-Snapshots vergleicht: einen direkt nach dem Laden der Anwendung, einen zweiten nachdem mehrfach zur verdächtigen Route navigiert und wieder weggenavigiert wurde. Zwischen den Snapshots sollte die Anzahl der Instanzen der betroffenen Komponentenklasse konstant bleiben. Steigt sie mit jeder Navigation, ist das ein eindeutiger Beweis für ein Leck, unabhängig davon, welcher konkrete Code dafür verantwortlich ist.

Die Devtools-Funktion "Comparison View" zeigt direkt, welche Objekttypen zwischen den beiden Snapshots zugenommen haben, sortiert nach Anzahl neuer Instanzen. Für Vue-Komponenten hilft die Suche nach dem Komponentennamen im Constructor-Filter, um gezielt zu prüfen, ob Instanzen einer bestimmten Komponente über die Navigation hinaus im Speicher bleiben. Der "Retainers"-Bereich zeigt anschließend die genaue Referenzkette, die verhindert, dass die Instanz vom Garbage Collector eingesammelt wird, meist direkt der entscheidende Hinweis auf den fehlenden Cleanup.


# Manual heap snapshot workflow in Chrome Devtools (no code, browser UI steps):
# 1. Open Devtools > Memory tab > select "Heap snapshot"
# 2. Take snapshot #1 right after initial page load
# 3. Navigate to the suspected route and back, 5-10 times
# 4. Force garbage collection (trash icon) before the next snapshot
# 5. Take snapshot #2, switch view to "Comparison"
# 6. Filter by component constructor name, check "# New" column
#    A steadily growing count across repeated cycles confirms a leak

7. Detached DOM Nodes als Leak-Indikator erkennen

Eine spezifische Kategorie von Vue Memory Leaks zeigt sich in Chrome Devtools als "Detached DOM tree", also DOM-Knoten, die aus dem sichtbaren Dokument entfernt wurden, aber weiterhin im Speicher existieren, weil irgendein JavaScript-Objekt noch eine Referenz darauf hält. Das passiert typischerweise, wenn eine Vue-Komponente eine DOM-Referenz über ref-Templates oder direktes querySelector() in einer Variable speichert, die den Komponenten-Lifecycle überlebt, etwa in einem Modul-Level-Cache oder einem globalen Event-Handler.

Der Heap-Snapshot-Filter "Detached" in Chrome Devtools listet genau diese verwaisten DOM-Bäume auf. Jeder Treffer sollte auf seine Retainer-Kette hin untersucht werden, um herauszufinden, welches JavaScript-Objekt die Referenz hält. In den meisten Fällen führt die Spur zurück zu genau denselben Ursachen wie bei anderen Vue Memory Leaks: ein Event-Listener, der auf ein DOM-Element statt auf window registriert wurde, oder eine Referenz, die versehentlich in einem langlebigen Objekt gespeichert blieb.

8. Drittanbieter-Bibliotheken und deren eigene Cleanup-Pflicht

Nicht jeder Fall von Vue Memory Leaks entsteht im eigenen Code. Drittanbieter-Bibliotheken für Charts, Maps oder Rich-Text-Editoren erzeugen häufig eigene interne Event-Listener, Web Worker oder Timer, die eine explizite destroy()- oder dispose()-Methode erfordern. Wird eine solche Bibliothek in onMounted() initialisiert, aber die zugehörige Zerstörungsmethode in onUnmounted() vergessen, bleibt die komplette interne Instanz der Bibliothek im Speicher, oft deutlich größer als der eigentliche Vue-Komponentencode.

Vor dem Einsatz einer neuen Bibliothek lohnt sich ein Blick in deren Dokumentation gezielt nach Begriffen wie "destroy", "dispose" oder "cleanup". Bibliotheken ohne dokumentierte Zerstörungsmethode sind ein Warnsignal und sollten vor dem produktiven Einsatz mit genau dem Heap-Snapshot-Workflow aus dem vorherigen Abschnitt getestet werden, um sicherzustellen, dass sie keine Vue Memory Leaks in der eigenen Anwendung verursachen.

9. Leak-Ursachen und Fixes im Vergleich

Die folgende Tabelle ordnet die häufigsten Ursachen von Vue Memory Leaks ihrem jeweiligen Cleanup-Pattern zu.

Ursache Symptom im Heap-Snapshot Cleanup-Pattern
Event-Listener auf window/document Wachsende Listener-Anzahl removeEventListener mit gleicher Referenz
setInterval / setTimeout Timer-Callbacks laufen nach Unmount weiter clearInterval / clearTimeout in onUnmounted
Externe Subscriptions Wachsende Komponenten-Instanzen unsubscribe() in onUnmounted
DOM-Referenzen in globalem Scope Detached DOM tree Referenz explizit auf null setzen
Drittanbieter-Bibliotheken Große fremde Objektbäume destroy()/dispose() in onUnmounted

Diese Tabelle eignet sich als Checkliste beim Code-Review: Jede neue Komponente mit addEventListener, setInterval, externen Subscriptions oder Drittanbieter-Initialisierung sollte gegen die passende Zeile geprüft werden, bevor Vue Memory Leaks überhaupt in Produktion auftauchen.

Mironsoft

Vue-Performance-Audits und Memory-Profiling

Vue Memory Leaks lassen eure Anwendung nach Stunden einfrieren?

Wir analysieren eure Vue-Anwendung mit Heap-Snapshots, finden vergessene Event-Listener, Timer und Subscriptions und liefern konkrete Cleanup-Fixes statt vager Performance-Tipps.

Heap-Snapshot-Analyse

Systematischer Vergleich über mehrere Navigationszyklen

Cleanup-Audit

Event-Listener, Timer und Subscriptions gegen onUnmounted prüfen

Drittanbieter-Check

Bibliotheken auf fehlende destroy()-Aufrufe prüfen

10. Zusammenfassung

Die überwiegende Mehrheit der Vue Memory Leaks lässt sich auf drei wiederkehrende Ursachen zurückführen: vergessene Event-Listener ohne spiegelbildliches Cleanup, nicht gestoppte Timer und externe Subscriptions, die nicht abbestellt werden. In allen drei Fällen ist das Fixmuster identisch: Jede Registrierung in onMounted() bekommt eine exakt passende Deregistrierung in onUnmounted(), mit identischer Funktionsreferenz statt einer neu erzeugten Kopie.

Wo Vermutungen allein nicht ausreichen, liefern Chrome-Devtools-Heap-Snapshots belastbare Beweise: Ein Vergleich zwischen zwei Snapshots nach wiederholter Navigation zeigt exakt, welche Komponenteninstanzen im Speicher verbleiben und über welche Retainer-Kette sie festgehalten werden. Wer diesen Workflow beherrscht, muss Vue Memory Leaks nicht mehr raten, sondern kann sie mit denselben Werkzeugen finden, die auch für Produktionsanalysen genutzt werden.

Vue Memory Leaks aufspüren — Das Wichtigste auf einen Blick

Event-Listener spiegeln

Jedes addEventListener in onMounted braucht ein removeEventListener in onUnmounted mit identischer Funktionsreferenz.

Timer und Subscriptions stoppen

clearInterval und unsubscribe() gehören zwingend in onUnmounted, sonst laufen sie nach dem Unmount weiter.

Heap-Snapshots vergleichen

Zwei Snapshots nach mehrfacher Navigation zur verdächtigen Route vergleichen. Wachsende Instanzanzahl bestätigt das Leck.

Drittanbieter nicht vergessen

Chart- und Editor-Bibliotheken brauchen oft eine explizite destroy()-Methode, die leicht übersehen wird.

11. FAQ: Vue Memory Leaks aufspüren und beheben

1Was ist die häufigste Ursache für Memory Leaks?
Vergessene Event-Listener auf window/document ohne passendes removeEventListener. Halten über Closures oft die ganze Komponente fest.
2Warum entfernt removeEventListener nichts?
Neue Arrow-Function beim Entfernen zählt als andere Referenz. Exakt dieselbe Funktionsreferenz muss übergeben werden.
3Räumt Vue watch() automatisch auf?
Ja, im Setup-Kontext. Kritisch nur außerhalb davon oder bei extern verwalteten Subscriptions.
4Wie erkenne ich einen Leak in Devtools?
Zwei Heap-Snapshots nach mehrfacher Navigation vergleichen. Stetig wachsende Instanzanzahl bestätigt den Leak.
5Was ist ein Detached DOM Tree?
Entfernte DOM-Knoten, die durch eine gespeicherte Referenz im Speicher bleiben, typischerweise durch eine langlebige ref-Referenz.
6Müssen Drittanbieter-Bibliotheken manuell aufgeräumt werden?
Meist ja, über eine destroy()- oder dispose()-Methode, aufgerufen in onUnmounted().
7Wie finde ich das haltende Objekt?
Im Heap-Snapshot den Retainers-Bereich öffnen. Zeigt die exakte Referenzkette zum Garbage-Collector-Blocker.
8Verursachen Closures automatisch Leaks?
Nicht automatisch, aber langfristig gespeicherte Callbacks halten mehr Kontext fest als nötig.
9Wie teste ich mehrere Timer?
IDs in einem Array sammeln, in onUnmounted per Schleife durchgehen. Kein Timer wird vergessen.
10Reicht Testing im Dev-Modus?
Nein, häufige Reloads verhindern Akkumulation. Wiederholte Navigation über längere Zeit ist realistischer.