Skip to content
BloGrove
mobile dev

React Native Performance: The Fundamentals That Fix 90% of Jank

Why React Native apps feel slow — the JS thread, list virtualization, image handling, and re-render control, plus a diagnostic workflow for real bottlenecks.

BBloGrove Editorial3 min read
React Native Performance: The Fundamentals That Fix 90% of Jank

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:

  1. 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.
  2. 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 in memo
  • 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 raw Image for 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.runAfterInteractions for 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:

  1. Enable Perf Monitor / React DevTools profiler during the exact interaction that stutters — is the JS thread red? Which component renders longest?
  2. Reproduce on a low-end device (or Android emulator with constrained cores) — flagship phones hide everything
  3. Binary-search the screen: comment out halves of the suspect component tree until the stall disappears — brutal and effective
  4. 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

Enjoyed this article?

Share it with your network.

Share

Keep reading

React Native Push Notifications: The Architecture to Ship
mobile dev

React Native Push Notifications: The Architecture to Ship

Local vs remote push in React Native — how the payload flows, why push isn't a background worker, permission wrangling, and patterns users won't mute.

5 min read
Native, Cross-Platform, or PWA: Choosing Where Your App Lives
mobile dev

Native, Cross-Platform, or PWA: Choosing Where Your App Lives

A framework for choosing in 2026 — what native, cross-platform, and progressive web apps genuinely cost and deliver, matched to team and distribution needs.

4 min read
PostgreSQL Indexes Explained: B-Trees and Beyond
databases

PostgreSQL Indexes Explained: B-Trees and Beyond

How Postgres indexes really work — B-tree mechanics, when indexes get ignored, covering, partial, and composite strategies, proven with EXPLAIN.

3 min read