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.
Inhaltsverzeichnis
- 1. Warum Memory Leaks in Single-Page-Apps besonders schmerzhaft sind
- 2. Vergessene Event-Listener als häufigste Ursache
- 3. Offene Timer und Intervals
- 4. Watcher und Subscriptions ohne Cleanup
- 5. Closures und versehentlich festgehaltene Referenzen
- 6. Heap-Snapshots in Chrome Devtools aufnehmen
- 7. Detached DOM Nodes als Leak-Indikator erkennen
- 8. Drittanbieter-Bibliotheken und deren eigene Cleanup-Pflicht
- 9. Leak-Ursachen und Fixes im Vergleich
- 10. Zusammenfassung
- 11. FAQ
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.