Skip to content
Back to Blog
Mobile Development

Memory Optimization Techniques for React Native Applications

5 min read
By Skynor Labs Team
Memory Optimization Techniques for React Native Applications

An app that uses more memory after opening a feed has not necessarily leaked. It may have cached images, retained a navigation screen or stored fetched records for reuse. The problem is whether that memory is useful, bounded and appropriate for the devices the product supports.

Start with a repeatable journey. A single screenshot of a memory graph rarely identifies the owner of an allocation or explains why it remains alive.

Establish what grows

Choose a flow such as opening a feed, viewing a detail screen, returning and switching tabs. Use the same account, dataset and build for each run.

Record the initial state, the peak while the flow runs and the settled state after returning. Repeat the journey several times. Allow for asynchronous cleanup and garbage collection; retained memory does not have to fall immediately after a navigation action.

  • Growth levels off after a few passes: investigate a bounded cache or retained screen, and check the memory budget.
  • JavaScript objects accumulate on every pass: inspect retaining paths, subscriptions, timers and application caches.
  • Process memory grows while the JavaScript heap is stable: investigate images, native views, media resources and native libraries.
  • The process terminates during a long session: inspect device logs and memory-pressure evidence before assigning a cause.

These observations narrow the investigation. They are not a diagnosis by themselves.

Measure JavaScript and native memory separately

The React Native DevTools Memory panel supports JavaScript heap snapshots and allocation timelines. Compare snapshots around the repeated journey, then inspect which objects remain reachable and what retains them.

A JavaScript snapshot does not account for every native allocation. Use Xcode's resource and memory investigation tools or the Android Studio memory profiler when investigating overall process growth. Decoded images and native media resources can matter even when the JavaScript heap looks unchanged.

Debugging and profiling add overhead. Investigate with the appropriate tools, then verify the user-visible result in a release build on representative physical devices.

Give each subscription an owner

A component that creates a listener should also remove it when its ownership ends. Here is a small hook with one AppState subscription and a matching cleanup:

import { useEffect, useState } from 'react';
import { AppState, type AppStateStatus } from 'react-native';

export function useAppVisibility() {
  const [state, setState] = useState<AppStateStatus>(AppState.currentState);

  useEffect(() => {
    const subscription = AppState.addEventListener('change', setState);
    return () => subscription.remove();
  }, []);

  return state;
}

Apply the same ownership rule to timers, socket subscriptions and native observers. Cancel an in-flight request where the API supports cancellation, and prevent an obsolete response from replacing the current screen's data.

React's effect lifecycle includes cleanup before an effect is recreated with changed dependencies. Its extra development setup-and-cleanup cycle helps expose incomplete cleanup; disabling that check does not solve a retained subscription.

Check navigation lifetime

A screen can remain mounted while another screen covers it. An unmount cleanup therefore may not stop polling, camera use or subscriptions when the screen loses focus.

Use the navigator's documented focus lifecycle for work that should run only while a screen is active. Keep a separate unmount cleanup for resources owned by the component. Confirm the behavior for stack navigation, tab switches and app backgrounding; those events have different meanings.

The React Navigation lifecycle guide explains why navigating away does not always unmount a screen. Retaining a screen is often intentional, so removing it should be a product decision rather than a blanket memory fix.

Bound image and data retention

A small compressed download can still require substantial memory when decoded. Request image dimensions appropriate to the display and device density from your image service. Setting a small view size alone does not ensure that the requested source is small.

Choose an image-cache policy deliberately. Expo Image distinguishes disk and memory caching; a memory cache trades retention for faster reuse. Prefetch only the likely next images, and measure the effect before extending prefetching across an entire feed.

Virtualized lists reduce the number of mounted rows. They do not automatically discard all records retained in your query cache or state. Decide how much previously fetched data to keep, how to revisit older pages and which cache entries can expire.

Tune the list's render window against both memory and fast-scroll behavior. The FlatList configuration guide also warns that clipping detaches views rather than deallocating them; it is not a general leak fix.

Confirm the fix survives a longer session

Repeat the original journey after changing one suspected owner. Compare the retained objects or native allocations, not just the peak graph. Then test a longer session with image-heavy screens, backgrounding and a return to the same flow.

Document the build, device, dataset and measured result. A lower peak that creates blank list rows or repeated downloads may be an unacceptable tradeoff. The useful outcome is bounded memory with the intended navigation, cache and offline behavior preserved.

Our React Native performance audit can assess a reproducible memory problem in an existing app. Send the affected flow and device details to define an investigation.

Discuss your engineering priorities

Tell us what your product does, what is getting in the way and what you need to ship next. We can review the scope together.

Discuss a project