skip to content

A React Native image-heavy travel feed crashes after about ten minutes of scrolling on low-end Android phones; how do you find and fix the cause?

level: seniorimportance: must knowfreq 48%

answer

  1. reproduce on the weakest device, release build
  2. crash log: out of memory or killed
  3. which memory grows: JS or native
  4. images versus retained JavaScript
  5. soak test until memory plateaus

basics

~20 s

Reproduce it in a release build on a low-end phone, confirm from the crash log that memory ran out, then see which memory grows: native memory points to oversized image decodes, a growing JavaScript heap to leaked listeners, timers or closures.

solid answer

~50 s

First I reproduce it on the weakest supported Android phone in a release build and read logcat: an `OutOfMemoryError` or the system killing the process for low memory confirms it is memory, not a JavaScript exception. Then I split the problem. Android Studio's Memory Profiler shows whether native and graphics memory climb; in parallel I log `performance.memory.usedJSHeapSize` or compare heap snapshots in a debug build to see whether the JavaScript heap climbs. Native growth on an image feed usually means full-resolution decodes, too many rows mounted, or caches holding originals, so I switch to thumbnails sized to the card and check list windowing. JavaScript growth means something retains old data — an uncleaned subscription or interval, a module-level cache, a closure holding the feed array — which heap snapshot comparison pins down. I verify with a ten-minute soak test: memory should rise, then plateau.

go deeper

for a junior

Know that crashes after minutes of use usually mean memory growth, and that low-end Android phones hit the limit first.

for a middle

Explain the two kinds of growth — native image memory and JavaScript heap — and which tool shows each one.

for a senior

Walk through reproduce, confirm, split, fix and soak-test on a release build and a low-end device, and name the fixes for each kind of growth.

for a principal

Discuss which devices define the memory floor, how memory soak tests become part of release checks, and how image delivery is owned across teams.

## Why it dies after ten minutes, and only on low-end Android A crash that appears only **after minutes of use** is the signature of **growth**: something accumulates with each scroll, screen visit or refresh until a limit is hit. It appears on **low-end Android** first because those devices have the least memory: less RAM, a smaller per-app budget, and a system that kills memory-hungry apps sooner. The same leak on a flagship phone may never reach the limit in a normal session. For an image-heavy feed there are two usual suspects: - **Native image memory** — decoded bitmaps at roughly 4 bytes per pixel, full-resolution originals, too many rows mounted, and caches holding recent images. - **JavaScript heap growth** — listeners and timers never removed, module-level arrays and caches that only grow, and closures that keep old feed pages alive. ## Step 1: reproduce under realistic conditions 1. **Use a release build** — debug builds carry dev-mode overhead and different memory behaviour. 2. **Use the weakest phone you support**, not an emulator on a laptop. 3. **Script the session** if you can: scroll the feed, open and close a few destinations, refresh — the same way each time, for ten minutes or more. ## Step 2: confirm it is memory Read the device log (logcat) around the crash: - a **`java.lang.OutOfMemoryError`** means an allocation failed, often while decoding a bitmap; - a process **killed by the system for low memory** shows up as the app disappearing rather than a JavaScript error. A JavaScript exception with a stack trace is a different bug; symbolicate it and fix it on its own terms. ## Step 3: find which memory grows | Signal | Where to look | Points to | |---|---|---| | Native / graphics memory climbs steadily | Android Studio Memory Profiler | decoded images, caches, native leaks | | JavaScript heap climbs across repeated visits | `performance.memory.usedJSHeapSize`, heap snapshots | retained JavaScript objects | | Both climb together | both | JavaScript retaining views or image references | `performance.memory` exposes `usedJSHeapSize`, `totalJSHeapSize` and `jsHeapSizeLimit` in React Native, and it works in release builds, so you can log a heap trend during the soak test. Heap snapshots come from React Native DevTools, which only runs in debug builds — the retained objects are the same, only the sizes differ. ## Step 4: fix what you found **Native image growth:** - request images sized to the card — layout size times `PixelRatio.get()` — instead of originals; - keep lists windowed so off-screen rows unmount and their images can be released; - keep full resolution for the single-photo detail screen only. **JavaScript growth:** - call `remove()` on every subscription and `clearInterval` / `clearTimeout` on every timer in effect cleanups; - bound module-level caches (keep the last N pages, not every page ever loaded); - avoid long-lived closures, such as a handler registered on a global emitter, that capture the whole feed array. ## Instrumenting the soak run A few cheap measurements turn a vague "it crashes eventually" into a curve: - **Log `performance.memory.usedJSHeapSize` every 30 seconds** from the release build, tagged with the screen and scroll position. - **Capture the Android Studio memory timeline** for the same run, so native growth lines up with the JavaScript numbers. - **Count what is mounted** — for example rows rendered by the feed — to see whether windowing actually releases rows. - **Note the time and action at the crash**, so you know which interaction pushed memory over the limit. ## Step 5: prove it Re-run the same scripted session on the same device. Healthy memory **rises, then plateaus** as caches fill and old items are released; a leak **keeps climbing**. Record the before and after curves so the fix is evidence, not a feeling. ## Traps - **Raising the heap limit** (such as `largeHeap` in the Android manifest) delays the crash on some devices and hides the growth. - **Testing only on a flagship phone**, where the limit is never reached. - **Reading only the JavaScript heap** when the problem is native image memory, or the reverse. - **Blaming the garbage collector.** If memory is still reachable, no collector can free it.

  • The React Native feed's JavaScript heap is flat but native memory climbs with scrolling. What do you check first?
    Image sizes and what stays mounted. Compare each image's source pixel dimensions with its card size times `PixelRatio.get()`; full-resolution originals in small cards are the usual cause. Then check that the list unmounts far-off rows, and that the detail view's full-size photos are released when the user leaves.
  • Why confirm a React Native memory fix with a soak test rather than a single heap reading?
    Memory legitimately rises while caches fill, so one reading cannot tell healthy growth from a leak. Running the same scripted session for ten minutes or more before and after the fix shows the curve's shape: a plateau means memory is being reused and released; a steady climb means something is still retained.
  • Can a React Native JavaScript leak cause native memory to grow as well?
    Yes. If JavaScript keeps references to mounted components or to image-bearing items that stay rendered, the native views and their decoded images stay alive too. That is why both curves climbing together often points back to retained JavaScript rather than to the image pipeline itself.

saying these in an interview costs you the question

  • Set largeHeap in the manifest and the crash is fixed
  • If the JavaScript heap is flat there cannot be a leak
  • An emulator on a laptop reproduces low-end phone memory limits
  • Memory rising at all during a session proves a leak
  • The garbage collector will eventually free anything no longer on screen