Back to Blog

React Native Performance: JS Thread, Hermes Engine, Profiling, and Flipper

Optimize React Native app performance in 2026 — JS thread vs UI thread, Hermes engine benefits, FlatList optimization, re-render reduction with memo/useCallback

Viprasol Tech Team
June 11, 2026
13 min read

React Native Performance: JS Thread, Hermes Engine, Profiling, and Flipper

React Native runs your JavaScript on a separate thread from the UI. When the JS thread is busy — computing, parsing JSON, running business logic — it can't respond to gestures or animations. This is the root cause of most React Native jank.

Understanding the threading model and knowing where to look for bottlenecks will let you fix most performance issues methodically rather than by guessing.


The Threading Model

React Native has three threads that matter for performance:

ThreadWhat Runs ThereBlocked By
JS ThreadYour React components, state updates, business logicHeavy computation, large JSON.parse, unoptimized re-renders
UI Thread (Main)Native view rendering, touch events, animationsJS thread blocking the bridge, native module calls
Shadow ThreadYoga layout calculationsComplex view hierarchies

The bridge (old architecture) serializes data between JS and native as JSON strings. Every prop update, every event, crosses the bridge. The New Architecture (JSI) replaces this with direct JS → native calls — no serialization overhead.

Practical implication: Animations that run on the JS thread will drop frames if JS is busy. Move animations to the native thread using Animated.useNativeDriver: true or Reanimated 3.


Enable Hermes

Hermes is a JavaScript engine optimized specifically for React Native. Enable it if you haven't — it reduces startup time 40–60%, lowers memory usage, and enables better profiling.

// android/app/build.gradle
android {
  defaultConfig {
    ...
  }
  buildTypes {
    release {
      // Hermes is enabled by default in RN 0.70+
      // Verify it's not disabled:
    }
  }
}
# ios/Podfile
use_react_native!(
  :path => config[:reactNativePath],
  :hermes_enabled => true,  # Ensure this is true
)
// Verify Hermes is running at runtime
if (global.HermesInternal) {
  console.log('Hermes is enabled ✓');
} else {
  console.warn('Hermes is NOT enabled — check your build config');
}

Hermes benefits in practice:

  • TTI (time to interactive): 40–60% faster on Android
  • Memory: 20–30% lower heap usage
  • Bundle: Hermes compiles JS to bytecode at build time — no JIT warmup on device

🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript — production-grade stack
  • Fixed-price contracts — no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Reducing Re-renders

The most common React Native performance problem on the JS thread is unnecessary re-renders:

// ❌ Recreated on every parent render — child re-renders unnecessarily
function OrdersList({ tenantId }: { tenantId: string }) {
  const onPress = (orderId: string) => {
    navigation.navigate('OrderDetail', { orderId });
  };

  return (
    <FlatList
      data={orders}
      renderItem={({ item }) => <OrderCard order={item} onPress={onPress} />}
    />
  );
}

// ✅ Stable references — child only re-renders when data changes
const OrderCard = React.memo(function OrderCard({
  order,
  onPress,
}: {
  order: Order;
  onPress: (id: string) => void;
}) {
  return (
    <Pressable onPress={() => onPress(order.id)}>
      <Text>{order.id}</Text>
    </Pressable>
  );
});

function OrdersList({ tenantId }: { tenantId: string }) {
  const navigation = useNavigation();

  // Stable reference — won't change between renders
  const handlePress = useCallback((orderId: string) => {
    navigation.navigate('OrderDetail', { orderId });
  }, [navigation]);

  // Stable keyExtractor
  const keyExtractor = useCallback((item: Order) => item.id, []);

  return (
    <FlatList
      data={orders}
      renderItem={({ item }) => <OrderCard order={item} onPress={handlePress} />}
      keyExtractor={keyExtractor}
    />
  );
}

Detecting re-renders:

// Debug component: logs on every render
function useRenderCount(componentName: string) {
  const count = useRef(0);
  count.current++;
  if (__DEV__) {
    console.log(`[Render] ${componentName}: ${count.current}`);
  }
}

// Or use React DevTools Profiler — available in Flipper

FlatList Optimization

FlatList is the most performance-sensitive component in most apps. Key props:

<FlatList
  data={orders}
  renderItem={renderItem}
  keyExtractor={keyExtractor}

  // Virtualization tuning
  initialNumToRender={10}        // Items rendered on first mount
  maxToRenderPerBatch={5}        // Items rendered per batch (lower = smoother scroll)
  windowSize={10}                // Render window in screen heights (default 21 — often too large)
  updateCellsBatchingPeriod={50} // Ms between render batches

  // Performance flags
  removeClippedSubviews={true}   // Unmount off-screen items (Android; use carefully on iOS)

  // Avoid anonymous functions in renderItem
  // ❌ renderItem={({ item }) => <OrderCard order={item} />}
  // ✅ renderItem={renderItem} where renderItem is stable (useCallback or module-level)

  // Prevent scroll jank with getItemLayout if item heights are fixed
  getItemLayout={(_, index) => ({
    length: ORDER_CARD_HEIGHT,    // Fixed height
    offset: ORDER_CARD_HEIGHT * index,
    index,
  })}
/>

🚀 Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Animations: Always Use Native Driver

import Animated, { useSharedValue, withSpring, useAnimatedStyle } from 'react-native-reanimated';

// ✅ Reanimated 3 — runs entirely on UI thread, no JS bridge
function AnimatedCard({ visible }: { visible: boolean }) {
  const opacity = useSharedValue(visible ? 1 : 0);

  useEffect(() => {
    opacity.value = withSpring(visible ? 1 : 0);
  }, [visible]);

  const animatedStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
    transform: [{ scale: opacity.value }],
  }));

  return (
    <Animated.View style={[styles.card, animatedStyle]}>
      <Text>Content</Text>
    </Animated.View>
  );
}

// If using the legacy Animated API:
const fadeAnim = useRef(new Animated.Value(0)).current;

Animated.timing(fadeAnim, {
  toValue: 1,
  duration: 300,
  useNativeDriver: true,  // ✅ Always true when animating opacity/transform
}).start();

// useNativeDriver: false is required only for layout properties (width, height, margin)
// which can't be offloaded to UI thread

Profiling with Flipper

Flipper is the standard React Native debugging tool. Key plugins for performance:

React DevTools (built in):

  • Components tab: inspect component tree, see props/state
  • Profiler tab: record renders, see what re-rendered and why

Hermes Debugger / JS profiler:

1. Open Flipper
2. Connect your device/simulator
3. Go to: Hermes Debugger → Performance
4. Click "Record"
5. Perform the action you want to profile
6. Click "Stop"
7. Inspect the flame graph:
   - Wide bars = slow functions
   - Long call stacks = deep recursion
   - Look for: JSON.parse, re-renders, heavy computations

Performance Monitor (in-app):

// Add to your dev menu or debug screen
import { PerformanceObserver } from 'react-native';

// Or use the built-in perf monitor
// Shake device → "Show Perf Monitor"
// Shows: JS FPS, UI FPS, RAM, CPU
// Target: both FPS above 55; RAM under 200MB

Heavy Computation: Move Off JS Thread

// ❌ Large JSON parsing on JS thread blocks UI
const handleResponse = async (response: Response) => {
  const text = await response.text();
  const data = JSON.parse(text);  // Blocks JS thread for large payloads
  setData(data);
};

// ✅ Use react-native-workers (Web Workers for React Native)
import { createWorker } from 'react-native-workers';

const worker = createWorker(() => {
  self.onmessage = (e) => {
    const parsed = JSON.parse(e.data);
    self.postMessage(parsed);
  };
});

const handleResponse = async (response: Response) => {
  const text = await response.text();
  worker.postMessage(text);
  worker.onmessage = (e) => setData(e.data);
};

// ✅ For image processing / heavy computation: use Expo TaskManager
// or native modules (Swift/Kotlin) — JS is the wrong layer for CPU-bound work

Bundle Size Optimization

# Analyze bundle size
npx react-native bundle \
  --platform android \
  --dev false \
  --entry-file index.js \
  --bundle-output /tmp/bundle.js

# Check bundle size
wc -c /tmp/bundle.js

# Visualize with source-map-explorer
npx react-native bundle \
  --platform android \
  --dev false \
  --entry-file index.js \
  --bundle-output /tmp/bundle.js \
  --sourcemap-output /tmp/bundle.map

npx source-map-explorer /tmp/bundle.js /tmp/bundle.map

Common bundle bloat culprits:

  • moment.js (~230KB) → replace with date-fns (tree-shakeable) or dayjs (~2KB)
  • Importing entire lodash (import _ from 'lodash') → use import { debounce } from 'lodash-es'
  • Large icon libraries → use only what you need with custom icon sets
  • Unused screens in navigation — lazy load with React.lazy + Suspense

New Architecture (JSI + Fabric)

The New Architecture (stable in RN 0.74+) eliminates the JSON bridge:

Old ArchitectureNew Architecture
JS ↔ Bridge (JSON serialization) ↔ NativeJS ↔ JSI (direct C++ call) ↔ Native
Async onlySynchronous calls possible
Serialization overheadZero serialization
Shadow thread separateFabric renders synchronously

Enable in new projects (already default in RN 0.74+):

# ios/Podfile
use_react_native!(
  :path => config[:reactNativePath],
  :new_arch_enabled => true,
)

Libraries must be migrated to support the New Architecture. Check compatibility at: reactnative.directory


Working With Viprasol

We build and optimize React Native applications — architecture reviews, performance audits, Hermes migration, New Architecture adoption, and custom native modules when JS performance isn't sufficient.

Talk to our team about mobile app development and optimization.


See Also

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms — we build it all with Next.js and React.

Free consultation • No commitment • Response within 24 hours

Viprasol · Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.