React Native Performance Best Practices That Work

The fastest React Native apps are usually the simplest ones. Here’s what actually improves performance before you waste weeks tuning.

By Tushar Goyal
EngineeringMobileTechnology
React Native Performance Best Practices That Work

title: "React Native Performance Best Practices That Work" author: "Tushar Goyal" date: "2026-04-11" excerpt: "The fastest React Native apps are usually the simplest ones. Here’s what actually improves performance before you waste weeks tuning." categories:

  • Engineering
  • Mobile
  • Technology

Most React Native performance problems are self-inflicted, and 80% of them come from unnecessary re-renders, oversized lists, and shipping too much JavaScript on the first screen.

That’s the good news. You usually do not need to eject, rewrite in native, or spend two weeks profiling GPU traces to make your app feel fast.

We shipped Uniffy as a cross-platform React Native app in 2 months, and we chose React Native over Flutter for one simple reason: the client’s team already knew React. That decision mattered more than theoretical framework benchmarks because onboarding speed beats micro-optimizations when you are trying to get an MVP into users’ hands.

The same logic applies to performance work. Fix the things users feel first, not the things engineers like debating on X.

Start with the JS thread, not native modules

If your React Native app feels janky, assume the JavaScript thread is overloaded until proven otherwise.

React Native performance is mostly about keeping three things under control:

  • Your initial render must stay small, because forcing too much work into the first screen makes the app feel slow before users even interact.
  • Your lists must render predictably, because unstable list items create dropped frames far faster than most animation bugs.
  • Your state updates must stay localized, because broad re-renders punish every interaction.

A useful target is simple:

  • Keep interactive screen transitions under 200ms perceived delay, because anything beyond that starts feeling sticky.
  • Keep list item renders cheap enough that a fast scroll does not trigger visible blanking or input lag.
  • Keep your bundle lean enough that low-end Android devices can parse and execute it without choking.

The first thing we do on any React Native project is identify what runs on every render. That usually reveals the real problem faster than any tooling.

Here’s what to check immediately:

  • Inline objects and functions inside large lists are a performance tax, because they create new references every render and break memoization.
  • Global state wired into the whole screen tree is usually overkill, because a tiny update at the top can force half the screen to re-render.
  • Derived values computed during render are often hidden bottlenecks, especially when they involve sorting, filtering, or formatting large arrays.

A bad pattern looks like this:

<FlatList
  data={items}
  renderItem={({ item }) => (
    <Card
      item={item}
      style={{ marginBottom: 12 }}
      onPress={() => handleOpen(item.id)}
    />
  )}
/>

A better version looks like this:

const styles = StyleSheet.create({
  cardSpacing: { marginBottom: 12 },
})

const onOpen = useCallback((id: string) => {
  handleOpen(id)
}, [handleOpen])

const renderItem = useCallback(
  ({ item }) => (
    <Card
      item={item}
      style={styles.cardSpacing}
      onPress={onOpen}
    />
  ),
  [onOpen]
)

<FlatList data={items} renderItem={renderItem} />

This will not save a broken architecture, but it removes pointless work. That matters because small inefficiencies multiply inside lists.

FlatList optimization matters more than most “advanced” tuning

If your app has feeds, chats, marketplaces, dashboards, or search results, list performance is the battle.

Founders often ask about animations first. That is usually the wrong place to start. A laggy list will make the entire product feel cheap even if every animation is perfectly tuned.

Here’s the counterintuitive part: rendering less data usually improves user experience more than rendering faster. If the user only needs to see 8 items, do not optimize for 50.

These are the FlatList settings we reach for first:

<FlatList
  data={items}
  renderItem={renderItem}
  keyExtractor={(item) => item.id}
  initialNumToRender={8}
  maxToRenderPerBatch={8}
  windowSize={5}
  removeClippedSubviews={true}
  updateCellsBatchingPeriod={50}
/>

Why these matter:

  • initialNumToRender={8} keeps first paint lighter, which helps your first screen feel responsive on mid-range devices.
  • maxToRenderPerBatch={8} prevents React Native from doing too much work in one go during scrolling.
  • windowSize={5} reduces memory pressure, which matters more on Android devices with limited RAM.
  • removeClippedSubviews helps long lists by unmounting content outside the viewport, though you should test it carefully on complex layouts.

Then fix the item component itself:

  • Wrap expensive row components in React.memo, but only after ensuring their props are stable. Memoization with unstable props is fake optimization.
  • Avoid images with unknown dimensions inside rows, because layout shifts create extra work and make scrolling feel inconsistent.
  • Pre-format data before it reaches the list when possible, because date formatting and string manipulation inside each row adds up quickly.

If you know item heights, use getItemLayout.

const ITEM_HEIGHT = 72

<FlatList
  data={items}
  renderItem={renderItem}
  getItemLayout={(_, index) => ({
    length: ITEM_HEIGHT,
    offset: ITEM_HEIGHT * index,
    index,
  })}
/>

That one change can make long lists dramatically smoother because React Native no longer has to guess layout positions while scrolling.

On products with dense mobile interfaces, this beats flashy optimization work every time.

Reduce app work before you “optimize” app work

Most teams try to make slow code run faster. The better move is to stop doing the work at all.

This principle shows up across our projects. On Harmony.ai, the biggest cost driver was prompt token count, not model choice. We reduced cost and improved response speed by caching intermediate outputs instead of recomputing the full chain every time. Mobile performance works the same way: remove repeated work first, then optimize what remains.

For React Native, that means being aggressive about simplification:

  • Do not load heavy data on app launch if the user does not need it on the first screen. Fetch for the current task, not for hypothetical future navigation.
  • Do not keep huge objects in global state if only one screen needs them. Broad shared state increases subscription churn and creates avoidable re-renders.
  • Do not trigger network requests from multiple nested components for the same resource. Centralize the fetch and cache the result.

A solid approach for startup apps looks like this:

  • Use React Query or a similar cache layer, because deduped requests and stale-while-revalidate patterns reduce both network chatter and UI instability.
  • Split screens into coarse containers and memoized presentational blocks, because that gives you clearer control over what should update.
  • Defer non-critical work until after interactions, because users care more about tapping smoothly than background bookkeeping finishing 300ms earlier.

For deferred work, InteractionManager is still useful:

useEffect(() => {
  const task = InteractionManager.runAfterInteractions(() => {
    preloadRecommendations()
  })

  return () => task.cancel()
}, [])

That pattern is especially useful for:

  • Preloading secondary content after the first screen is stable.
  • Running non-urgent transforms that would otherwise compete with touch responsiveness.
  • Deferring analytics side effects that do not need to block UI.

Image handling is another common failure point.

  • Large, uncompressed images will wreck scroll performance faster than most state bugs. Resize them server-side before they ever reach the device.
  • Use modern formats and caching aggressively, because downloading the same oversized image twice is just paying for your own mistake again.
  • Define image dimensions up front, because layout uncertainty causes measurable jank in feed-style screens.

Measure the right things and ignore vanity optimizations

If you are not testing on a low-to-mid Android device, you are not testing React Native performance seriously.

A lot of apps feel fine on an iPhone 15 Pro and fall apart on a 3-year-old Android phone. Founders miss this because the team is building on premium hardware. Users are not.

Our rule is simple:

  • Profile the screens that make money or retention, not the settings page no one opens.
  • Test cold start, first navigation, feed scroll, typing latency, and image-heavy screens before anything else.
  • Track whether changes improve felt performance, not just profiler screenshots.

The most useful checks are boring:

  • Turn on the performance monitor and watch for JS frame drops during real interactions.
  • Record a slow device session and note exactly where taps, scrolls, or transitions feel delayed.
  • Remove one suspected bottleneck at a time so you can see what actually moved the needle.

A counterintuitive take: premature native module work is usually a distraction.

If your app is still re-rendering 100 list rows on every state update, writing custom native code is not the smart move. Fixing render scope, list virtualization, and data loading patterns will produce bigger gains in less time.

That was the same lesson behind our stack decision on Uniffy. We did not pick the theoretically fastest option. We picked the one that let the team ship confidently and maintain performance with the skills they already had.

For most startups, that is the right bar. A fast-enough app shipped this month beats a perfect app still stuck in optimization discussions next quarter.

What to Do Next

Run one performance pass on your three most-used screens this week, and do it in this order:

  • Audit every FlatList and reduce initial render size, batch size, and unnecessary row complexity.
  • Inspect which components re-render on tap, scroll, and input, then stabilize props and isolate state updates.
  • Defer non-critical work until after interactions, and remove any launch-time fetches that do not affect the first screen.
  • Test the result on a mid-range Android device, because that is where performance mistakes become obvious.

If you only do one thing, fix your lists first. That is where the biggest gains usually are.

If you're at this stage, schedule a call with us.