Skip to content
khua
← All writing

Article · July 19, 2026

Why Your React Native App Is Slow (It Is Almost Never the Framework)

Five causes account for nearly every performance complaint I have been called in to fix. None of them are the bridge, and all of them are your code.By Khua · 3 min read

I get asked to look at slow React Native apps fairly often. The diagnosis is boringly consistent. In order of how often I find it:

1. The list is re-rendering everything

FlatList is fast. FlatList with an inline renderItem arrow function that closes over state is not, because every parent render produces a new function identity and every row re-renders.

// Every keystroke in the search box re-renders 400 rows.
<FlatList data={items} renderItem={({ item }) => <Row item={item} onPress={() => select(item.id)} />} />

Hoist renderItem, memoise Row, and pass a stable keyExtractor. Pass the id to a stable callback rather than creating a closure per row. This one fix has accounted for more perceived speed than every other item on this list combined.

2. Everything lives in one context

A single context holding the whole app state means any change re-renders every consumer. Contexts are not a state manager; they are a delivery mechanism. Split them by update frequency — the thing that changes on every keystroke should not share a provider with the thing that changes twice a session.

3. Animations are running in JavaScript

If an animation is driven by setState or by Animated without useNativeDriver, it runs on the JS thread and competes with everything else you are doing. Reanimated's worklets run on the UI thread and keep animating even while JS is busy.

The tell is an animation that stutters exactly when data arrives. That is not the animation's fault.

4. Images are full resolution

A 3000px JPEG rendered into a 120px avatar decodes at full size before it is scaled. Multiply by a scrolling list and you have a memory problem that presents as jank. Resize server-side, or use a CDN that takes dimensions in the URL.

5. The startup path does too much

Everything imported at module scope runs before the first frame. Analytics SDKs, font loading, a require of a 2MB JSON file — all of it lands between the splash screen and the first paint. Defer what is not needed to render the first screen.

How to find it rather than guess

Turn on the performance monitor and watch the two frame rates separately. UI dropping while JS holds means a rendering or image problem. JS dropping means you are blocking the JavaScript thread, and the cause is items 1, 2 or 5.

Guessing at this is how people end up rewriting in another framework and shipping the same bug.


If you have an app that got slow and you would rather not spend a fortnight bisecting it, that is a thing I do.