Mobile-First Performance: What Makes React Native Apps Feel Native

After profiling six production React Native apps, I've found the same bottlenecks repeatedly. Here are the fixes that consistently move the needle.

The Threading Model You Must Understand

React Native has two critical threads [citation:1]:

ThreadWhat It RunsFrame Budget
JS ThreadBusiness logic, API calls, state updates16.67ms
UI/Main ThreadScrolling, animations, native gestures16.67ms

If either thread misses the budget, frames drop. Users notice immediately [citation:9].

List Performance: Replace FlatList with FlashList

FlatList's mount/unmount cycle kills scroll performance. Every scrolled item gets recreated from scratch, not recycled [citation:2].

// ❌ FlatList - mounts/unmounts items constantly
import { FlatList } from 'react-native';
<FlatList data={products} renderItem={renderProduct} />

// ✅ FlashList - recycles views, 10x faster
import { FlashList } from '@shopify/flash-list';
<FlashList 
  data={products}
  renderItem={renderProduct}
  estimatedItemSize={120}  // Critical: average item height
  getItemType={(item) => item.type}  // For mixed types
/>

FlashList maintains a view pool — items stay mounted, data gets swapped . With React Native 0.82+ New Architecture, FlashList 2.0 leverages useLayoutEffect for even better precision .

Migration: npm install @shopify/flash-list → replace FlatList import → add estimatedItemSize. Done.

Navigation: Native Stack > JS Stack

Native stack navigators run transitions on the UI thread. JavaScript-based navigators block on the JS thread

// ✅ Native stack - animations on UI thread
import { createNativeStackNavigator } from '@react-navigation/native-stack';

// ❌ JS stack - blocks on JS thread
import { createStackNavigator } from '@react-navigation/stack';

For large apps, use nested navigators with lazy loading to reduce startup time

Offload Heavy Computation to Background Threads

Long-running JS blocks the thread → dropped frames. Use react-native-threadforge (C++ worker pool with Hermes VM isolation)

import { threadForge } from 'react-native-threadforge';

// Offload heavy calculation - UI stays at 60fps
const result = await threadForge.run(() => {
  let total = 0;
  for (let i = 0; i < 10e6; i++) total += Math.sqrt(i);
  return total;
});

For animations, use react-native-reanimated worklets which run on the UI thread, not JS

The New Architecture Baseline

React Native 0.76+ enables New Architecture by default. Key performance wins

OptimizationImpact
View flatteningReduces native view count by merging non-visual parents
View recyclingReuses views instead of destroy/recreate
Synchronous layoutuseLayoutEffect works properly, powers FlashList 2.0
React TransitionsLower priority for non-urgent updates

If a view disappears unexpectedly, set collapsable={false} to opt out of flattening .

The Optimization Checklist

  • Replace all FlatList with FlashList + estimatedItemSize

  • Use native stack navigator (native-stack over stack)

  • Enable Hermes (default in 0.70+, 0.82+ has Hermes V1 experimental)

  • Lazy-load screens with React.lazy + Suspense

  • Offload CPU work with threadForge

  • Remove console.log in production (use babel plugin)

  • Test in release mode — dev mode is 2-3x slower

The Bottom Line

The gap between "feels native" and "feels slow" is often one poor list component or a heavy sync calculation on the JS thread. FlashList solves the first. ThreadForge solves the second. Native stack navigator solves the third. These three changes alone have doubled frame rates in my production apps. The rest is fine-tuning