React Native – Performance

August 9, 20264 min readUpdated 8/24/2026

There are two threads. Your JavaScript runs on one; the UI is drawn on the other. Almost every performance problem in React Native is the first one being too busy to answer the second in time.

What actually makes an app feel slow

In rough order of how often it is the culprit:

Too much work per render, usually a list re-rendering every row when one changed. A long list without virtualisation. An animation on the JavaScript thread. Large images. A slow startup, which is the one users judge you on hardest.

Notice what is not on the list: the framework. If your app is janky, it is almost certainly something above and not React Native being React Native.

memo and useCallback are a pair

This is the single most useful optimisation, and the one most often applied incorrectly.

export const ProductCard = memo(function ProductCard({ product, onSelect }: Props) {

memo skips re-rendering a component when its props are unchanged, compared shallowly. The menu renders a card per product, so without it, opening the cart sheet — which changes state in a parent — re-renders every card even though not one of their props changed.

But it only works if the props are referentially stable. An inline arrow is a new function on every render, so every card sees a "changed" prop and re-renders anyway. Hence:

  const handleSelect = useCallback((product: Product) => {
    setSelectedProduct(product);
    setOpenCount((count) => count + 1);
  }, []);

memo without useCallback achieves nothing except an extra comparison on every render. Applying one and not the other is the most common way this optimisation is wasted — and it looks like it should be working, which is what makes it hard to spot.

Same reasoning for style objects: StyleSheet.create at module level is stable; an inline style={{ padding: 8 }} is a new object every render.

Do not reach for it by default

Every memo costs a prop comparison, and every useCallback costs a dependency check. Use them where components are numerous, expensive, or both — list rows, mainly. Sprinkling them everywhere makes the code harder to read and measurably slower.

React 19's compiler changes this calculus: where it is enabled it inserts memoisation for you, and hand-written useCallback becomes redundant. Worth knowing before you spend a day adding them by hand.

Lists

Covered in lesson 7 and it belongs on any performance list. FlatList over ScrollView; getItemLayout when rows are a fixed height; never a FlatList inside a ScrollView in the same direction, which disables virtualisation entirely.

Animations

useNativeDriver: true moves an animation to the UI thread, where a busy JavaScript thread cannot stutter it. Only transform and opacity qualify — which is why animating a layout property is the slow choice and a transform that looks the same is the fast one. Lesson 10 covers it.

Context re-renders

A context value that changes re-renders every consumer. Two habits: split contexts by how often they change, and memoise the value so the provider re-rendering does not by itself invalidate everyone.

The demo app splits auth, menu, cart and toasts precisely so a cart change does not re-render everything that only cares about the signed-in user.

Images

The most common cause of memory pressure. A 4000×3000 photo rendered into a 100dp thumbnail still decodes at full size — resize on the server, or use a CDN that does. expo-image adds caching, better decoding and placeholders, and is usually worth swapping in.

Startup time

The metric users actually notice, and the one no in-app profiler shows you.

Hermes is the biggest lever and is on by default now: it can pre-compile your JavaScript to bytecode, so startup skips parsing entirely. Ship less JavaScript — route-level code splitting matters here as on the web. Do not block the first render on a network call; render the shell and fill it in.

The demo app holds its splash screen until it knows whether anyone is signed in, which is a deliberate trade: a slightly longer splash instead of a visible flash from signed-out to signed-in. Holding it for anything slower than a keychain read would be the wrong call.

Measure, do not guess

React DevTools Profiler shows which components rendered and why — start here, because "why did this re-render" is the question you usually have.

The performance monitor in the dev menu shows both frame rates. If the UI thread holds 60 and the JS thread drops, the problem is your code; if the UI thread drops, it is usually rendering or images.

Always profile a release build. Development has StrictMode double-rendering, unminified code and no bytecode precompilation. A debug build is dramatically slower than what you ship, and optimising against it means optimising the wrong thing.

What is next

Error Boundaries and Failure States — a blank screen is the one thing a user cannot refresh away.