Memory leaks show up as a slow climb in usage during long sessions, jank that worsens over time, and eventually out-of-memory crashes — usually on lower-end Android devices first. They are almost always missing cleanup: something subscribed, scheduled, or retained that was never released.

Key takeaways

  • Inspect timers, subscriptions, listeners, large images, and retained navigation screens.
  • Reproduce memory growth through repeatable user flows instead of isolated screen tests.
  • Verify fixes on physical Android and iOS devices with release-like builds.

Find what is being retained

Start with the usual suspects in effects: setInterval/setTimeout without clearTimeout, event listeners and subscriptions without an unsubscribe in the effect cleanup, and async work that calls setState after the component unmounts. Every useEffect that subscribes must return a cleanup function.

On the native side, use Android Studio's Memory Profiler and Xcode Instruments (Allocations and Leaks) to capture heap snapshots before and after a repeated flow. A steadily growing retained set across identical navigations points straight at the leak.

Reproduce and verify on real devices

Build a repeatable flow — for example, push and pop the same screen 50 times — and watch memory between iterations. Large images that are not resized, caches without eviction, and screens kept mounted by the navigator are common amplifiers.

Confirm fixes on physical devices with release-style builds. The JS dev tooling hides native retention, and the emulator's generous memory can mask a leak that crashes a real low-memory phone.