Back to Notes

React Native Performance Optimization

A pillar guide for reducing startup time, improving rendering, and fixing the most common performance bottlenecks.

Most React Native performance problems trace back to one of three things: too much work on the JS thread, too much re-rendering, or too much happening at startup before the user can do anything. Here's how I actually diagnose and fix each of those, in the order I check them.

Start With Startup Time, Not Renders

Before touching a single component, I profile cold start. Users decide whether an app feels fast within the first few seconds, and no amount of list-virtualization work matters if the splash screen sits there for four seconds first.

The usual culprits are large synchronous imports pulled in before the first screen renders, unoptimized images bundled into the app binary instead of loaded on demand, and native module initialization that blocks the JS thread. I split the bundle so the first screen only imports what it needs, defer analytics/SDK initialization until after the first paint, and check whether Hermes is actually enabled — it still isn't the default in every template, and it makes a measurable difference in parse and startup time versus JSC.

  • Enable Hermes and confirm it's actually active (check via `global.HermesInternal`)
  • Defer non-critical SDK/analytics initialization until after first paint
  • Lazy-load screens with `React.lazy` / navigation-level code splitting where the navigator supports it
  • Audit bundled image sizes — oversized PNGs in the app bundle are a common, easy win

Re-renders Are the Next Suspect

Once startup is reasonable, the next place performance dies is unnecessary re-renders cascading through a screen. The fix isn't "wrap everything in memo" — over-memoizing has its own cost and makes code harder to read. I look for the actual source: a parent re-rendering on every keystroke because state lives too high in the tree, inline function/object props breaking memoization on children that would otherwise skip, or context values that change reference on every render even when the meaningful data didn't change.

React DevTools' profiler (or `why-did-you-render` for a more surgical look) tells me exactly which components re-render and why, rather than guessing. I fix the actual cause — usually moving state down closer to where it's used, or splitting a large context into smaller ones — before reaching for `useMemo`/`useCallback` as a patch.

Lists Are Their Own Category

Long lists are the single most common place I see production apps stutter. `FlatList` with correct `keyExtractor`, `getItemLayout` when row heights are fixed or predictable, and conservative `windowSize`/`maxToRenderPerBatch` tuning solves most of it. For lists with genuinely complex rows or very large datasets, `FlashList` from Shopify is usually a straightforward drop-in replacement that recycles views instead of re-mounting them, and it's the first thing I reach for now on any list that scrolls noticeably worse than native.

The other list killer is doing expensive work inside `renderItem` — inline style objects created every render, unmemoized child components, or images without a fixed size that trigger layout recalculation as they load. Fixed dimensions on list images and memoized row components fix this almost every time.

Animations on the UI Thread, Not the JS Thread

Any animation driven by JS-thread state updates competes with your business logic for the same thread, and it shows as dropped frames the moment anything else is happening — a network response coming in, a list scrolling. React Native Reanimated moves the actual animation work to the UI thread via worklets, so it keeps running smoothly even when the JS thread is busy. For gesture-driven interactions especially — swipe-to-dismiss, drag handles, pull-to-refresh — pairing Reanimated with React Native Gesture Handler instead of the built-in PanResponder is the difference between a native-feeling interaction and a laggy one.

Measure, Don't Guess

The pattern across all of this: profile first, fix the actual bottleneck, then measure again. React DevTools Profiler, Flipper (or its successors), and just watching Xcode Instruments / Android Studio Profiler on a real mid-range device catch problems that a simulator on a fast dev machine hides completely. A screen that feels instant on an M-series Mac simulator can visibly stutter on a three-year-old Android phone, and that's the device your actual users are probably on.

Sohaib Hussain Shah

Sohaib Hussain Shah

React Native & Software Engineer

Building Something Amazing?
Let's chat.

I build production-grade mobile and web apps with React Native, React, and TypeScript. I'm looking for remote roles with high-growth tech companies that value quality and scale.