Zum Inhalt springen
Core Web Vitals Spezialisten
Frontend-Optimierung

Speicherlecks im Frontend: wenn die Seite träge wird

Die Oberfläche startet schnell und wird nach zwanzig Minuten zäh: wie Speicherlecks im Frontend entstehen, wie man sie im Verlauf misst und wieder loswird.

12 Min. Lesezeit FrontendJavaScriptINPSaaSSpeicher

Der erste Eindruck stimmt: Die Anwendung ist in unter zwei Sekunden da, die Ansicht steht, Klicks kommen sofort an. Zwei Stunden später sitzt dieselbe Person vor derselben Oberfläche und wartet nach jedem Klick einen Wimpernschlag zu lang. Am Code hat sich nichts geändert, am Netz nichts, am Server nichts -- geändert hat sich nur die Sitzungsdauer. Genau dieser Verfall über die Zeit ist der blinde Fleck der üblichen Tempomessung: Ein Prüflauf öffnet die Seite, misst die ersten Sekunden und schließt sie wieder. Er sieht den Zustand, in dem die Anwendung am flottesten ist -- nicht den, in dem Ihre Nutzer den größten Teil ihres Arbeitstags verbringen. Rund 23 Prozent (Web Almanac 2025) der mobilen Seiten verfehlen die INP-Schwelle von 200 Millisekunden, und ein Teil davon fällt erst im Laufe einer Sitzung durch. Dahinter steckt typischerweise dasselbe Muster: Die Seite gibt Speicher nicht mehr frei, den sie längst nicht mehr braucht. Dieser Beitrag zeigt, wie solche Lecks im Frontend entstehen, warum sie ausgerechnet die Reaktionszeit treffen, wie man sie über den Verlauf einer Sitzung misst statt in einer Momentaufnahme -- und wie man sie wieder loswird. In unserer Frontend-Optimierung ist das ein wiederkehrender Befund bei Oberflächen mit langen Arbeitssitzungen.

Das Wichtigste in Kürze

  • Tempoverlust muss nicht beim ersten Laden entstehen. Eine Oberfläche kann schnell starten und nach zwanzig Minuten Arbeit spürbar zäh reagieren -- der Auslöser ist Speicher, der über die Sitzung hinweg nicht freigegeben wird.
  • Fünf Ursachen decken den Großteil der Fälle ab: nicht abgemeldete Ereignis-Zuhörer, abgehängte DOM-Bäume nach Ansichtswechseln, weiterlaufende Zeitgeber und Beobachter ohne disconnect, unbegrenzte Zwischenspeicher im Arbeitsspeicher und Bildpuffer in Tabellen mit Endlos-Nachladen.
  • Aussagekräftig ist der Verlauf, nicht der Einzelwert: Drei Speicherabbilder über eine lange Sitzung, die Zählung abgehängter Knoten und die Kurve der Ereignis-Zuhörer zeigen den Defekt, den ein Prüflauf mit Einzelaufruf in der Regel nicht sichtbar macht.
  • Im Feld liefert performance.measureUserAgentSpecificMemory() seit Chrome 89 (MDN) einen Schätzwert für den Speicherverbrauch einer Seite; die Schnittstelle arbeitet asynchron und setzt eine Cross-Origin-Isolation voraus (MDN).
  • Ein Wert unter 200 Millisekunden (web.dev) gilt als gute Interaction to Next Paint. Längere Aufräumläufe der Speicherbereinigung laufen auf demselben Hauptstrang wie die Eingabeverarbeitung und schieben jede Reaktion nach hinten.

Wenn die Oberfläche erst nach zwanzig Minuten träge wird

Ein Browser gibt Speicher nicht auf Zuruf frei. Die Speicherbereinigung der JavaScript-Laufzeit räumt genau das weg, was von keiner Stelle im Programm mehr erreichbar ist -- alles andere bleibt liegen, auch wenn es fachlich längst überflüssig ist. Ein Speicherleck im Frontend ist deshalb selten ein Fehler der Laufzeit, sondern fast durchweg eine vergessene Referenz: ein Zuhörer, der noch am Dokument hängt, ein Zeitgeber, der weiterläuft, ein Objekt in einer Liste, die keiner leert. Mit jedem Ansichtswechsel kommt etwas hinzu, und die Grundlast steigt Stufe um Stufe. Sichtbar wird das nicht als Absturz, sondern als Zähigkeit: Die Aufräumläufe werden länger und häufiger, und weil sie auf demselben Hauptstrang laufen, der auch Klicks und Tastendrücke verarbeitet, verschiebt sich jede Reaktion um ein paar Millisekunden nach hinten. Ein Wert unter 200 Millisekunden (web.dev) gilt als gute Interaction to Next Paint, gemessen am 75. Perzentil (web.dev) der Interaktionen; darüber wird die Verzögerung spürbar.

Das Tückische ist die Messlücke. Ein Prüflauf mit Einzelaufruf lädt die Seite in einem frischen Profil, misst die ersten Sekunden und beendet die Sitzung wieder -- er sieht ausschließlich den günstigsten Zustand der Anwendung. Genau deshalb bleibt ein Leck in solchen Berichten unauffällig, während es im Alltag jede Sachbearbeiterin trifft, die die Oberfläche acht Stunden lang offen hat. Zwar erreichen 77 Prozent (Web Almanac 2025) der mobilen Seiten eine gute Interaction to Next Paint, doch diese Verteilung stammt aus kurzen Besuchen. Bei einer Fachanwendung mit langen Sitzungen verschiebt sich das Bild deutlich, sobald man statt der ersten Minute die vierzigste betrachtet. Wie man Labor- und Feldwerte sauber auseinanderhält, beschreibt der Beitrag zu RUM, CrUX und Felddaten.

Speicherleck ist nicht gleich Speicherverbrauch

Eine Anwendung, die viel Speicher belegt, hat deshalb noch kein Leck. Entscheidend ist der Verlauf: Liegt die Grundlast nach jedem Aufräumlauf höher als nach dem vorigen, wird etwas festgehalten, das freigegeben werden müsste. Bleibt der Sockel dagegen konstant, arbeitet die Anwendung schlicht mit einem großen, aber stabilen Datenbestand. Die Frage lautet also nicht 'wie viel Speicher', sondern 'wie entwickelt sich der Sockel über die Sitzung'.

Fünf Ursachen, die wir regelmäßig finden

In Projekten mit Fachoberflächen und Portalen tauchen im Kern typischerweise dieselben fünf Muster auf. Erstens nicht abgemeldete Ereignis-Zuhörer: Eine Komponente hängt sich an window, document oder einen globalen Ereignisbus, wird beim Ansichtswechsel aus dem Dokument entfernt und meldet sich dabei nicht ab -- der Zuhörer hält die gesamte Komponente samt ihrer Daten am Leben. Zweitens abgehängte DOM-Bäume: Ein Teilbaum verschwindet aus dem Dokument, aber eine Variable, ein Zwischenspeicher oder ein Schließungsumfeld hält weiterhin eine Referenz darauf; der Knoten ist unsichtbar und belegt trotzdem Speicher. Drittens weiterlaufende Zeitgeber und Beobachter: setInterval ohne clearInterval, ein MutationObserver, ResizeObserver oder IntersectionObserver ohne disconnect -- sie arbeiten nach dem Ansichtswechsel munter weiter und verbrauchen Rechenzeit für eine Ansicht, die längst geschlossen ist. Viertens unbegrenzte Zwischenspeicher im Arbeitsspeicher: eine Map, die Serverantworten sammelt, ein Protokollpuffer, eine Rückgängig-Historie ohne Obergrenze. Fünftens Bildpuffer in Tabellen mit Endlos-Nachladen: Jede nachgeladene Seite hängt weitere Zeilen und weitere dekodierte Bilder an, während die längst herausgescrollten liegen bleiben.

Diese fünf Muster unterscheiden sich in ihrer Wirkung. Zuhörer und Beobachter kosten zusätzlich Rechenzeit, weil sie bei jedem passenden Ereignis erneut ausgeführt werden -- sie treiben also nicht nur den Speicher, sondern unmittelbar die Reaktionszeit. Abgehängte Knoten und unbegrenzte Zwischenspeicher wirken langsamer, dafür stetig. Wichtig ist die Abgrenzung zu drei verwandten Themen: Beim Reduzieren der DOM-Größe geht es um die statische Knotenzahl einer Seite -- eine mobile Median-Seite kommt auf 594 HTML-Elemente, am 90. Perzentil sind es 1.716 (Web Almanac 2024). Beim Aufteilen langer Aufgaben mit scheduler.yield() geht es um die Länge einzelner Arbeitspakete, beim Auslagern in Web Worker um den Ort, an dem gerechnet wird. Hier dagegen geht es um Verfall: Alle drei Kennzahlen können am Anfang einer Sitzung tadellos aussehen und trotzdem nach einer Stunde aus dem Ruder laufen.

Zuhörer, Zeitgeber und Beobachter abmelden

Der wirksamste Hebel ist zugleich der einfachste: Jede Anmeldung braucht eine Abmeldung, und beides gehört an dieselbe Stelle im Lebenszyklus einer Komponente. In der Praxis scheitert das selten an der Kenntnis, sondern an der Buchführung -- bei einem Dutzend Zuhörern pro Ansicht verliert man den Überblick, welcher wo wieder gelöst werden muss. Eine AbortController-Instanz je Ansicht nimmt diese Buchführung ab: Alle Zuhörer bekommen dasselbe Signal mitgegeben, und ein einziger Aufruf von abort() löst sie gemeinsam (MDN). Beobachter besitzen mit disconnect() ihre eigene Abmeldung, Zeitgeber werden mit clearInterval beziehungsweise clearTimeout beendet.

ansicht-lebenszyklus.js
function ansichtStarten(container) {
  const steuerung = new AbortController();
  const { signal } = steuerung;

  window.addEventListener('resize', neuBerechnen, { signal });
  document.addEventListener('keydown', tastenBefehl, { signal });

  const beobachter = new ResizeObserver(neuBerechnen);
  beobachter.observe(container);

  const takt = setInterval(datenHolen, 30000);

  // Beim Ansichtswechsel genau einmal aufrufen
  return function beenden() {
    steuerung.abort();
    beobachter.disconnect();
    clearInterval(takt);
    container.replaceChildren();
  };
}

Der Rückgabewert ist hier der entscheidende Teil: Die Funktion, die eine Ansicht öffnet, liefert gleich die Funktion mit, die sie wieder abräumt. Der Router oder das Komponenten-Gerüst muss dann nur noch dafür sorgen, dass diese eine Funktion bei jedem Ansichtswechsel aufgerufen wird -- statt dreizehn verstreuter Abmeldungen an dreizehn Stellen. Für Daten, die eine Komponente überdauern sollen, ohne sie am Leben zu halten, bieten sich WeakMap und WeakSet an: Sie halten ihre Schlüssel nur schwach, sodass die Speicherbereinigung ein Objekt einsammeln darf, sobald es sonst nirgends mehr referenziert wird (MDN). Ergänzend erlauben WeakRef und FinalizationRegistry aus ECMAScript 2021 gezielt schwache Verweise (MDN) -- ein Werkzeug für Sonderfälle, kein Ersatz für sauberes Abräumen.

Was beim Ansichtswechsel hängen bleibt

Ansichtswechsel sind der Moment, in dem am meisten schiefgeht -- besonders in Anwendungen, die ohne vollständigen Seitenaufbau zwischen Bereichen springen. Der alte Teilbaum verschwindet aus dem Dokument, aber nicht zwingend aus dem Speicher. Wie sich solche Routenwechsel überhaupt sauber erfassen lassen, behandelt der Beitrag zu Soft Navigations in Single-Page-Anwendungen. Drei Stellen sind dabei erfahrungsgemäß am ergiebigsten:

Abgehängte Knoten

Ein aus dem Dokument entfernter Teilbaum bleibt vollständig im Speicher, solange irgendeine Variable, ein Zwischenspeicher oder ein Schließungsumfeld noch auf ihn zeigt. Im Speicherabbild der Entwicklerwerkzeuge tragen solche Knoten den Vermerk 'Detached' und lassen sich dort direkt zählen (Chrome for Developers).

Unbegrenzte Zwischenspeicher

Eine Map, die jede Serverantwort behält, eine Rückgängig-Historie ohne Obergrenze, ein Protokollpuffer, der nur wächst: Jeder dieser Speicher bleibt harmlos, solange eine Sitzung kurz ist. Über Stunden wird daraus der größte Einzelposten im Heap. Abhilfe schafft eine feste Obergrenze mit Verdrängung der ältesten Einträge.

Bildpuffer in Endlos-Listen

Tabellen und Kachelansichten mit Endlos-Nachladen hängen mit jeder Seite weitere Zeilen und weitere dekodierte Bilder an. Dekodierte Bilddaten wiegen ein Vielfaches der übertragenen Datei, und die mobile Median-Seite bringt schon beim Laden 2.362 KB (Web Almanac 2025) mit. Abhilfe schafft Virtualisierung, wie sie der Beitrag zu content-visibility und gespartem Rendering beschreibt.

BeobachtungMomentaufnahme (Einzelaufruf)Verlauf über die Sitzung
Heap-Größeeinmalig kurz nach dem LadenSockel nach jedem Aufräumlauf
Abgehängte Knotenmeist nullwachsen mit jedem Ansichtswechsel
Ereignis-ZuhörerAusgangsbestandZuwachs je geöffneter Ansicht
ReaktionszeitZustand direkt nach dem Ladenverschlechtert sich stufenweise
Typischer BefundunauffälligGrundlast steigt Stufe um Stufe

Den Verlauf messen statt einer Momentaufnahme

Wer ein Leck finden will, braucht eine zweite Achse: die Zeit. Im Labor führt der Weg über eine bewusst lange, skriptgesteuerte Sitzung -- zwanzig bis vierzig Ansichtswechsel, dieselben Schritte in derselben Reihenfolge -- und über drei Speicherabbilder: eines direkt nach dem Laden, eines nach der Hälfte der Wechsel, eines am Ende, jeweils nach einem erzwungenen Aufräumlauf. Vergleicht man das dritte mit dem ersten, zeigt der Zuwachs an behaltenen Objekten sehr genau, was nicht freigegeben wurde; die Zählung der abgehängten Knoten ist dabei der schnellste Einstieg (Chrome for Developers). Ergänzend zeichnet das Leistungs-Panel den Heap, die Zahl der Dokumente, der DOM-Knoten und der Ereignis-Zuhörer als Kurve über die Zeit auf -- eine Zuhörerzahl, die mit jedem Ansichtswechsel um denselben Betrag steigt, ist ein eindeutiger Befund. Im Feld liefert performance.measureUserAgentSpecificMemory() seit Chrome 89 (MDN) einen Schätzwert für den gesamten Speicherverbrauch einer Seite; die Schnittstelle arbeitet asynchron und setzt eine Cross-Origin-Isolation der Seite voraus (MDN). Die ältere Eigenschaft performance.memory ist dagegen nicht standardisiert und nur in einem Teil der Browser vorhanden (MDN) -- als grober Trendwert brauchbar, als belastbare Kennzahl nicht. Wer den Zusammenhang zur Reaktionszeit unmittelbar sehen will, legt die Speicherkurve neben die Long Animation Frames, die seit Chrome 123 (Chrome for Developers) zeigen, welches Skript einen langen Frame verursacht hat.

Der Kern in einem Satz

Ein Speicherleck erkennt man nicht an einer hohen Zahl, sondern an einem steigenden Verlauf: Wenn der Sockel nach jedem Aufräumlauf höher liegt als nach dem vorigen, hält die Anwendung etwas fest, das sie längst loslassen dürfte.

Grenzen, Fehldiagnosen und Sonderfälle

Nicht jede steigende Kurve ist ein Leck. Die Speicherbereinigung räumt erst auf, wenn es sich lohnt, und lässt bis dahin bewusst Müll liegen -- eine über wenige Minuten wachsende Kurve kann schlicht bedeuten, dass noch kein Aufräumlauf nötig war. Aussagekräftig ist deshalb ausschließlich der Sockel unmittelbar nach einem Aufräumlauf, nicht der Spitzenwert dazwischen. Ebenso wenig taugt ein Vergleich über verschiedene Geräte hinweg: Der Speicherhaushalt einer Laufzeit richtet sich nach dem verfügbaren Arbeitsspeicher, und derselbe Code verhält sich auf einem Notebook anders als auf einem drei Jahre alten Diensthandy. Auch die Schätzwerte der Speicherschnittstellen sind bewusst ungenau, weil eine exakte Angabe Rückschlüsse auf fremde Inhalte im selben Prozess erlauben würde (MDN). Und schließlich: Wächst der Speicher, weil die Anwendung tatsächlich mehr Daten hält -- etwa eine geöffnete Tabelle mit zehntausend Zeilen --, ist das kein Defekt, sondern eine Frage der Darstellung, wie sie das Reduzieren der DOM-Größe behandelt.

Vier Fehlschlüsse, die Zeit kosten

Erstens: Ein hoher Speicherwert allein ist noch kein Befund -- entscheidend bleibt die Entwicklung des Sockels. Zweitens: Ein Prüflauf mit Einzelaufruf kann diesen Defekt kaum zeigen, weil er die Sitzung vor dem interessanten Teil beendet. Drittens: Ein erzwungener Aufräumlauf in den Entwicklerwerkzeugen verzerrt die Messung, wenn er nur vor einem der Abbilder ausgeführt wird -- er gehört vor jedes. Viertens: Ein Neuladen der Seite als vermeintliche Lösung verschiebt das Problem lediglich auf die nächste Sitzung und verdeckt es zugleich in den Felddaten.

In der eigenen Anwendung vorgehen

Der Einstieg braucht kein großes Projekt. Wichtig ist vor allem die Reihenfolge: erst reproduzieren, dann messen, dann beheben, dann im Feld nachhalten. Fünf Schritte reichen für den ersten Durchgang:

  1. Die typische Arbeitssitzung Ihrer Nutzer nachbauen: Welche Ansichten werden in welcher Reihenfolge wie oft gewechselt, und wie lange bleibt die Oberfläche offen?
  2. Diesen Ablauf skriptgesteuert zwanzig- bis vierzigmal wiederholen und drei Speicherabbilder aufnehmen, jeweils nach einem erzwungenen Aufräumlauf
  3. Den Zuwachs an behaltenen Objekten, abgehängten Knoten und Ereignis-Zuhörern zwischen dem ersten und dem letzten Abbild bestimmen
  4. Die gefundenen Referenzen an ihrem Ursprung auflösen: ein Aufräumpunkt je Ansicht, Obergrenzen für Zwischenspeicher, Virtualisierung für lange Listen
  5. Speicher und Reaktionszeit anschließend im Feld erheben -- erst dort zeigt sich, ob die lange Sitzung wirklich stabil bleibt

Genau so läuft eine Performance-Analyse bei uns ab, wenn der Verdacht auf Verfall über die Zeit besteht: Wir bauen die reale Arbeitssitzung nach, messen den Verlauf statt eines Einzelaufrufs, benennen die haltenden Referenzen namentlich und setzen die Aufräumpunkte dort, wo sie hingehören. Für SaaS-Oberflächen und B2B-Portale mit langen Arbeitssitzungen ergänzen wir das um eine Langzeitmessung, weil ein Werkzeug mit Einzelaufruf genau diesen Defekt in der Regel nicht sichtbar macht. Dass Frontend-Speicher und Serverlast zwei getrennte Baustellen sind, die im Alltag gern gemeinsam auftreten, zeigen die Beiträge zur PHP-Laufzeit mit OPcache, JIT und FPM-Workern und zum Lasttest vor der Hochsaison.

Der Unterschied zwischen einer schnellen und einer angenehmen Anwendung liegt selten in der ersten Sekunde. Er liegt in der vierzigsten Minute -- und dort entscheidet sich, ob jemand gern damit arbeitet oder jeden Nachmittag einmal neu lädt.

Projekterfahrung aus Performance-Projekten für Fachoberflächen

Der Aufwand für die Behebung fällt meist kleiner aus als befürchtet, weil sich die Ursachen auf wenige Stellen konzentrieren: ein Aufräumpunkt je Ansicht, eine Obergrenze je Zwischenspeicher, Virtualisierung für die eine lange Liste. Aufwendiger ist die Diagnose -- und genau dort lohnt ein systematisches Vorgehen statt Raterei. Wer die Reaktionszeit darüber hinaus im Zusammenhang verstehen möchte, findet die Einordnung im Beitrag zum Optimieren der Interaction to Next Paint. Damit ein einmal behobenes Leck bei der nächsten Auslieferung nicht zurückkehrt, gehört die Messung in die Baustrecke, wie es der Beitrag zu Performance-Budgets in der CI beschreibt. Welche Leistungen zu einem solchen Projekt gehören, zeigt die Übersicht der Performance-Leistungen.

Quellen und Studien

Dieser Artikel basiert auf Daten von web.dev, MDN Web Docs, Chrome for Developers und den Web-Almanac-Ausgaben 2024 und 2025 des HTTP Archive. Die genannten Zahlen beziehen sich auf den Stand der jeweiligen Veröffentlichung.

Verwandte Artikel