Native Memory-Profiling-Tools und ein systematischer Debugging-Workflow
Eine App, die nach einer halben Stunde Nutzung spürbar langsamer wird oder auf älteren Geräten abstürzt, hat mit hoher Wahrscheinlichkeit ein Speicherleck. Anders als bei reinem JavaScript-Code, wo der Garbage Collector viele Fehler still verzeiht, wirken sich in React Native sowohl JavaScript-seitige als auch native Referenzen auf den tatsächlichen Speicherverbrauch aus. Dieser Artikel beschreibt die typischen Ursachen wie nicht bereinigte Event-Listener, hängende Timer und problematische Closures, und zeigt, wie sich Lecks mit Xcode Instruments und dem Android Studio Profiler systematisch aufspüren und beheben lassen.
Inhaltsverzeichnis
- 1. Warum Speicherlecks in React Native besonders tückisch sind
- 2. Ursache 1: Nicht bereinigte Event-Listener
- 3. Ursache 2: Hängende Timer und Intervalle
- 4. Ursache 3: Closures, die Komponenten referenzieren
- 5. Diagnose mit Xcode Instruments auf iOS
- 6. Diagnose mit dem Android Studio Profiler
- 7. Ein systematischer Debugging-Workflow für Speicherlecks
- 8. Präventive Muster: WeakMap, Refs und AbortController
- 9. Grenzen der Diagnose und kontinuierliches Monitoring
- 10. Zusammenfassung
- 11. FAQ
1. Warum Speicherlecks in React Native besonders tückisch sind
React Native besteht aus mindestens zwei Speicherwelten, die getrennt verwaltet werden: dem JavaScript-Heap, den die JavaScript-Engine über Garbage Collection bereinigt, und dem nativen Speicher auf iOS und Android, der über Referenzzählung beziehungsweise eigene Garbage Collection funktioniert. Ein Objekt, das auf der JavaScript-Seite längst nicht mehr gebraucht wird, kann trotzdem im Speicher bleiben, wenn eine native Komponente, etwa eine Kartenansicht oder ein Videoplayer, weiterhin eine Referenz darauf hält. Diese Zweiteilung macht Speicherlecks in React Native schwerer zu diagnostizieren als in reinen Web-Anwendungen, weil ein einzelnes Profiling-Werkzeug selten beide Seiten gleichzeitig abbildet.
Besonders tückisch ist, dass viele Lecks erst nach wiederholter Navigation auffallen: Eine einzelne Bildschirmansicht mag beim ersten Öffnen unauffällig sein, aber wenn ein Nutzer zehnmal zwischen zwei Screens hin und her navigiert und der Speicherverbrauch dabei stetig steigt statt nach jedem Zurücknavigieren wieder auf ein stabiles Niveau zu fallen, deutet das auf ein systematisches Leck bei der Screen-Erstellung oder beim Unmounting hin. Genau dieses Muster, wiederholte Navigation und kontinuierlich steigender Speicherverbrauch, ist der zuverlässigste erste Indikator, bevor man überhaupt ein Profiling-Werkzeug öffnet.
2. Ursache 1: Nicht bereinigte Event-Listener
Der mit Abstand häufigste Grund für Speicherlecks in React-Native-Apps ist ein Event-Listener, der in useEffect registriert, aber in der Cleanup-Funktion nicht wieder entfernt wird. Das betrifft native Event-Emitter wie AppState, Dimensions, NetInfo oder eigene NativeEventEmitter-Instanzen genauso wie Listener auf externen Bibliotheken. Jede fehlende Rückgabe einer Cleanup-Funktion aus useEffect hält die betroffene Komponente über die Listener-Referenz im Speicher, selbst nachdem sie längst unmounted wurde, weil der native Emitter weiterhin eine Callback-Referenz auf die Komponente hält.
Besonders bei Bibliotheken, die einen globalen Singleton-Emitter verwenden, summiert sich dieser Fehler über die Zeit: Bei jeder Navigation zu einem Screen wird ein neuer Listener registriert, ohne dass der alte je entfernt wird, und nach zwanzig Navigationsvorgängen laufen zwanzig veraltete Callback-Funktionen parallel, jede mit einer eigenen Referenzkette auf ihre ursprüngliche Komponente. Ein systematischer Blick auf jede AppState.addEventListener-, Dimensions.addEventListener- oder DeviceEventEmitter.addListener-Aufrufstelle im Code ist deshalb der erste Schritt jeder Leck-Diagnose.
// Fehlerhaft: Listener wird nie entfernt
useEffect(() => {
const subscription = AppState.addEventListener("change", handleAppStateChange);
// fehlendes return -> Leck bei jedem Mount/Unmount-Zyklus
}, []);
// Korrekt: Cleanup-Funktion entfernt den Listener beim Unmount
useEffect(() => {
const subscription = AppState.addEventListener("change", handleAppStateChange);
return () => {
subscription.remove();
};
}, []);
// Native Event-Emitter mit expliziter Referenz
useEffect(() => {
const emitter = new NativeEventEmitter(NativeModules.LocationModule);
const sub = emitter.addListener("onLocationUpdate", handleLocationUpdate);
return () => {
sub.remove();
};
}, []);
3. Ursache 2: Hängende Timer und Intervalle
setTimeout und setInterval sind eine zweite häufige Fehlerquelle, besonders wenn ein Timer-Callback eine State-Update-Funktion einer bereits unmounteten Komponente aufruft. React selbst gibt bei diesem Muster in älteren Versionen eine Warnung aus, moderne React-Versionen unterdrücken sie zwar, das zugrunde liegende Speicherproblem bleibt aber bestehen: Der Timer hält über seinen Callback eine Referenz auf die Komponente und alle darin erfassten Variablen, bis er entweder abläuft oder explizit gecleart wird.
Besonders bei setInterval potenziert sich das Problem, weil ein nicht geclearter Interval-Timer dauerhaft weiterläuft und bei jedem Tick erneut versucht, auf eine längst nicht mehr existierende Komponente zuzugreifen, was zusätzlich CPU-Last erzeugt. Die zuverlässige Lösung ist, jede Timer-ID in einer Ref zu speichern und in der useEffect-Cleanup-Funktion konsequent mit clearTimeout beziehungsweise clearInterval aufzuräumen, unabhängig davon, ob der Timer zu diesem Zeitpunkt schon abgelaufen ist oder nicht.
4. Ursache 3: Closures, die Komponenten referenzieren
Subtiler als fehlende Listener-Cleanups sind Closures, die unbeabsichtigt eine ganze Komponente oder ein großes Objekt am Leben halten, obwohl nur ein kleiner Teil davon eigentlich gebraucht würde. Ein klassisches Beispiel ist ein Callback, der außerhalb einer Komponente in einem Modul-Level-Array oder einer Map gespeichert wird, etwa zur Registrierung eines globalen Handlers, und dabei über Closure Zugriff auf props oder lokale Variablen der ursprünglichen Komponente behält. Solange dieser Eintrag im globalen Array nicht explizit entfernt wird, verhindert er, dass der Garbage Collector die gesamte referenzierte Komponente samt ihres Komponentenbaums freigibt.
Ähnlich problematisch sind Callbacks, die an native Module übergeben werden und dort in einer nativen Datenstruktur zwischengespeichert bleiben, etwa ein Kamera-Callback, der bei jedem Aufnahmezyklus neu registriert, aber nie deregistriert wird. Da native Module aus JavaScript-Sicht eine Blackbox sind, bleibt dieses Leck bei einer reinen JavaScript-Heap-Analyse unsichtbar und wird erst im nativen Profiler erkennbar, was einer der Hauptgründe ist, warum eine reine Chrome-DevTools-Analyse für React-Native-Speicherlecks oft nicht ausreicht.
5. Diagnose mit Xcode Instruments auf iOS
Xcode Instruments bietet mit dem Leaks-Template und dem Allocations-Template zwei komplementäre Werkzeuge für die iOS-Speicheranalyse. Das Allocations-Template zeigt den Speicherverlauf über die Zeit als Graph und erlaubt es, Generationen zu markieren: Man löst eine Aktion aus, etwa das Öffnen und Schließen eines Screens zehnmal hintereinander, markiert davor und danach eine Generation und lässt Instruments nur die Objekte anzeigen, die zwischen beiden Markierungen entstanden sind und nicht wieder freigegeben wurden. Bleiben nach mehreren Wiederholungen konsequent Objekte übrig, die eindeutig zur untersuchten Ansicht gehören, liegt ein Leck vor.
Das Leaks-Template ergänzt das um eine automatische Erkennung von Referenzzyklen im nativen Objective-C- und Swift-Code, die in React-Native-Apps oft in eigenen nativen Modulen oder in Bridging-Code zwischen JavaScript und nativen Views auftreten. Für Hermes-basierte Apps zusätzlich relevant ist, dass der JavaScript-Heap selbst über den Hermes-eigenen Memory-Profiler untersucht werden muss, den man über die Chrome-DevTools-Verbindung oder über Flipper aktivieren kann; Instruments zeigt ausschließlich den nativen Speicher, nicht den internen JavaScript-Heap-Zustand.
# Xcode Instruments über die Kommandozeile für einen CI-Lauf starten
xcrun xctrace record --template "Leaks" \
--device "iPhone 15 Simulator" \
--launch -- /path/to/MyApp.app
# Ergebnis als .trace-Datei zur weiteren Analyse in Instruments öffnen
open MyApp_*.trace
6. Diagnose mit dem Android Studio Profiler
Der Android Studio Memory Profiler zeigt einen Live-Graphen des Java-Heaps, des nativen Heaps und weiterer Speicherkategorien und erlaubt es, jederzeit einen Heap-Dump zu erzeugen. Für die Leck-Diagnose bewährt sich derselbe Ansatz wie bei Instruments: eine Aktion mehrfach wiederholen, vor und nach den Wiederholungen einen Heap-Dump erzeugen, und die beiden Dumps im Vergleichsmodus gegenüberstellen. Objekte, deren Anzahl zwischen den Dumps unerwartet gestiegen ist, etwa Instanzen einer bestimmten Komponentenklasse oder eines Bitmap-Objekts, liefern den entscheidenden Hinweis auf die Leck-Quelle.
Ein für React-Native-Apps besonders relevantes Detail ist der native Heap, der auf Android separat vom Java-Heap ausgewiesen wird und häufig Bitmap-Speicher für Bilder sowie den Speicherverbrauch der JavaScript-Engine selbst enthält. Ein stetig wachsender nativer Heap bei stabilem Java-Heap deutet oft auf nicht freigegebene Bilddaten hin, etwa wenn eine Bildkomponente große Originalbilder statt bereits skalierter Varianten lädt und diese im Cache nicht begrenzt werden.
7. Ein systematischer Debugging-Workflow für Speicherlecks
Ein reproduzierbarer Workflow beginnt mit der Isolation: statt die gesamte App zu untersuchen, wird zuerst der konkrete Screen oder Nutzerfluss identifiziert, bei dem der Speicherverbrauch ansteigt, etwa durch gezieltes Beobachten des Speichergraphen während typischer Navigationspfade. Danach folgt die Wiederholung: derselbe Vorgang wird mindestens fünf- bis zehnmal ausgeführt, weil ein einzelner Durchlauf selten eindeutig zwischen normalem Speicherverbrauch und echtem Leck unterscheiden lässt, während ein linear ansteigender Trend über mehrere Wiederholungen ein klares Signal ist.
Im dritten Schritt folgt die Eingrenzung mit dem passenden nativen Profiler, Instruments auf iOS oder der Android Studio Profiler auf Android, um die konkreten Objekttypen zu identifizieren, die nicht freigegeben werden. Im letzten Schritt wird die Ursache im Code lokalisiert, meist über eine gezielte Suche nach addEventListener-, setInterval- oder addListener-Aufrufen im betroffenen Screen, gefolgt von einer erneuten Wiederholung des gesamten Workflows nach dem Fix, um zu bestätigen, dass der Speicherverbrauch jetzt tatsächlich stabil bleibt.
8. Präventive Muster: WeakMap, Refs und AbortController
Neben der reaktiven Fehlersuche lohnt sich der präventive Einsatz von Mustern, die Speicherlecks von vornherein erschweren. Eine WeakMap statt eines regulären Objekts oder Arrays für Zwischenspeicher, die Komponenten-Instanzen als Schlüssel verwenden, erlaubt dem Garbage Collector, Einträge automatisch zu entfernen, sobald die zugehörige Komponente andernorts nicht mehr referenziert wird. Für Callbacks, die an native Module übergeben werden, bewährt sich außerdem ein AbortController-Muster, bei dem ein Signal explizit anzeigt, ob eine noch laufende asynchrone Operation ihr Ergebnis überhaupt noch verarbeiten soll.
Für Timer- und Listener-Cleanup empfiehlt sich ein eigener Custom Hook, der die Registrierung und das Entfernen kapselt, sodass der Cleanup-Aufruf nicht bei jeder einzelnen Komponente erneut von Hand geschrieben und potenziell vergessen werden kann. Ein solcher Hook zentralisiert das korrekte Verhalten an einer Stelle und macht es für das gesamte Team einfacher, Speicherlecks strukturell zu vermeiden, statt sie erst im Profiler zu entdecken.
// Custom Hook kapselt Registrierung und garantierte Cleanup-Logik
function useAppStateListener(callback: (state: AppStateStatus) => void) {
const callbackRef = useRef(callback);
callbackRef.current = callback;
useEffect(() => {
const subscription = AppState.addEventListener("change", (state) => {
callbackRef.current(state);
});
return () => subscription.remove();
}, []);
}
// Verwendung: kein manuelles Cleanup mehr nötig, kann nicht vergessen werden
function ProfileScreen() {
useAppStateListener((state) => {
if (state === "active") {
refetchProfile();
}
});
return <ProfileContent />;
}
9. Grenzen der Diagnose und kontinuierliches Monitoring
Profiling-Sessions in Instruments oder im Android Studio Profiler finden Lecks zuverlässig, sind aber auf manuelle Testläufe angewiesen und decken selten alle Nutzerpfade einer echten App ab. Ein Leck, das nur bei einer selten genutzten Kombination aus Bildschirmen auftritt, bleibt im lokalen Profiling leicht unentdeckt, bis es in der Produktion als Crash-Häufung sichtbar wird. Deshalb ergänzt ein kontinuierliches Speicher-Monitoring in Produktion, etwa über native Crash-Reporting-Tools mit Out-of-Memory-Erkennung, die punktuelle lokale Diagnose sinnvoll.
Wichtig ist außerdem, Speicherlecks nicht mit hohem, aber stabilem Speicherverbrauch zu verwechseln: Eine Bildergalerie mit vielen großen Bildern verbraucht naturgemäß mehr Speicher als ein einfaches Formular, das ist kein Leck, solange der Verbrauch nach dem Verlassen des Screens wieder sinkt. Ein Leck liegt erst vor, wenn der Speicherverbrauch nach wiederholtem Verlassen und erneutem Betreten desselben Zustands kontinuierlich weiter ansteigt, statt zu einem stabilen Niveau zurückzukehren.
| Ursache | Typisches Symptom | Diagnose-Werkzeug | Fix-Ansatz |
|---|---|---|---|
| Event-Listener ohne Cleanup | Speicher steigt bei jeder Navigation | Code-Review, Instruments/Profiler-Vergleich | return-Cleanup in useEffect ergänzen |
| Nicht geclearte Timer | CPU-Last und Speicher steigen parallel | Android Studio Profiler CPU+Memory | clearTimeout/clearInterval in Cleanup |
| Closures mit globaler Referenz | Komponente bleibt trotz Unmount im Heap | Heap-Dump-Vergleich, Retain-Pfad prüfen | WeakMap statt Modul-Array verwenden |
| Native Callback ohne Deregistrierung | Nur im nativen Profiler sichtbar, JS-Heap stabil | Xcode Instruments Leaks-Template | AbortController-/Deregistrierungs-Muster |
| Ungecachte große Bilder | Nativer Heap wächst, Java-Heap stabil | Android Studio Profiler nativer Heap | Bildgrößen begrenzen, Cache-Strategie |
Mironsoft
React-Native-App-Entwicklung und Magento-Anbindung
Eine mobile App zum Magento-Shop, die wirklich rund läuft?
Wir entwickeln React-Native-Apps, die sauber an die Magento REST- oder GraphQL-API angebunden sind, von der ersten Codezeile bis zur Veröffentlichung im App Store und bei Google Play.
App-Konzeption
Architektur und Feature-Umfang einer Magento-angebundenen App gemeinsam planen.
Magento-API-Integration
Produktkatalog, Warenkorb und Checkout sauber an die Shop-API anbinden.
Store-Veröffentlichung
App Store- und Google-Play-Freigabeprozess ohne Stolperfallen begleiten.
10. Zusammenfassung
Speicherlecks aufspüren: Das Wichtigste auf einen Blick
Zwei Speicherwelten
JavaScript-Heap und nativer Heap werden getrennt verwaltet, Lecks können auf beiden Seiten entstehen.
Häufigste Ursachen
Fehlende Listener-Cleanups, hängende Timer und Closures mit globaler Referenz erklären die meisten Fälle.
Diagnose-Werkzeuge
Xcode Instruments für iOS, Android Studio Profiler für Android, jeweils mit Wiederholungs- und Vergleichsmethode.
Prävention
Custom Hooks für Listener-Cleanup und WeakMap-Muster verhindern viele Lecks bereits strukturell.