React Native apps don't feel slow because the framework is slow — they feel slow because of a handful of repeated mistakes, all fixable with fundamentals. The good news: jank has a short culprit list. The better news: diagnosing it is a workflow, not guesswork. Here's the model, the culprits in impact order, and how to find yours.
The mental model: two worlds and a bridge#
React Native runs your JavaScript on its own thread (the JS thread), while UI rendering and gestures happen natively (the UI thread) — coordinated through an async bridge passing serialized messages. Two consequences drive everything:
- Slow JS = dropped frames. Any long synchronous JS task — heavy computation, giant renders — blocks updates and animations from being scheduled. A phone's screen refreshes every ~16ms; miss the window and users see stutter.
- Crossing the bridge costs. Shipping huge data payloads or rapid-fire state updates across it creates serialization traffic jams.
So the diagnostic questions are always: what's hogging the JS thread? and what's crossing the bridge unnecessarily? Modern architectures (Fabric, JSI) reduce bridge overhead, but both rules still predict nearly every real-world jank.
Culprit #1: FlatList misuse (or not using it)#
The classic self-inflicted wound: rendering 500 items with .map() inside a ScrollView. That mounts every row immediately — memory balloons, first paint takes seconds.
// Wrong: everything mounted at once
<ScrollView>{items.map(i => <Row key={i.id} item={i} />)}</ScrollView>
// Right: only visible rows mount
<FlatList
data={items}
keyExtractor={i => i.id}
renderItem={({ item }) => <Row item={item} />}
getItemLayout={...} // skip measurement when rows have fixed height
/>
Then tune the knobs that matter: windowSize (render distance), initialNumToRender, and stable keyExtractor. For mixed-content feeds, FlashList-class libraries push further — but a properly configured FlatList fixes most scroll complaints alone.
Culprit #2: re-render storms#
React's rendering rules apply doubly on constrained mobile hardware:
- Contexts holding frequently-changing values re-render every consumer on every tick — timers, geolocation, sensor streams belong in leaf components, not global context
- Inline callbacks/objects (
onPress={() => ...}) defeat memoization on large lists; extract handlers for row components wrapped inmemo - Derived values computed inside render loops should be memoized only when measured expensive (profile first)
Culprit #3: images#
Images are the silent killer — unoptimized assets decode to enormous bitmaps on the JS/UI boundary:
- Serve appropriately-sized assets (a 4000px photo displayed at 100px wastes 99% of decode work)
- Use cached, prioritized image components (
expo-image,react-native-fast-image) instead of rawImagefor lists - Set explicit dimensions where possible; layout thrash from unknown sizes compounds scroll jank
Culprit #4: heavy JS on hot paths#
Anything synchronous and CPU-bound — parsing large JSON, formatting hundreds of dates, cryptography — blocks scheduling. The moves:
- Chunk it: break work across frames with scheduler APIs (
InteractionManager.runAfterInteractionsfor post-animation work) - Debounce/throttle input-driven work (typing handlers firing per keystroke)
- Move truly heavy computation off-thread — libraries with native modules, or a separate JS process
The diagnostic workflow#
Guesswork wastes days; measurement takes minutes:
- Enable Perf Monitor / React DevTools profiler during the exact interaction that stutters — is the JS thread red? Which component renders longest?
- Reproduce on a low-end device (or Android emulator with constrained cores) — flagship phones hide everything
- Binary-search the screen: comment out halves of the suspect component tree until the stall disappears — brutal and effective
- Fix the measured culprit, re-measure, stop. One bottleneck fixed beats five optimizations guessed.
The discipline matters more than any individual technique: teams that profile before optimizing ship smooth apps with plain code, while teams applying cargo-cult memo everywhere still ship jank — just less legibly.
What interviews expect#
Mobile-specific rounds probe exactly this material: explain the two threads, why ScrollView+map fails at scale, what keyExtractor does, how you'd debug scroll jank end-to-end. Answering with the workflow ("first I'd measure which thread...") signals experience far louder than reciting optimization lists.
Smoothness is mostly subtraction: fewer mounted nodes, fewer re-renders, smaller payloads across the bridge, sized images. Start there before reaching for anything exotic.
Related: React rendering model · TypeScript contracts · interview study plan