React Native Performance Oct 2024 · 6 min read

React
Native
60fps

Practical techniques for using Reanimated 3, FlatList optimization, and Hermes to squeeze buttery-smooth 60fps out of any device — even the cheap ones.

60
 LIVE FPS SIMULATION Optimized thread · 16ms budget
ZH
Zubair Hussain
Full Stack Developer · thezubairh@gmail.com
React Native Performance — Evolution Milestones
2018
Bridge Era
JS ↔ Native via async serialized bridge. 30fps ceiling.
2020
Hermes Ships
AOT bytecode. TTI drops 50%. Memory down 30%.
2022
Reanimated 2
Worklets on UI thread. Animations fully native.
NOW
2023–24
New Arch
JSI + TurboModules + Fabric. Zero bridge overhead.
2025
Static Hermes
Typed JS compilation. Near-native execution speed.
§ 01 — The Problem
01

Why Low-End Devices Choke

Most React Native performance advice is written for flagship phones. The real world is a $120 Android running on 2GB RAM and a quad-core from 2019. Drop below 60fps and users notice immediately — and they blame the app, not the hardware.

The culprit is almost always one of three things: JavaScript thread congestion, bridge thrashing for animations, or FlatList rendering too many items at once. Each has a surgical fix.

A dropped frame costs you 33ms. At 30fps you've already spent your entire animation budget on the previous frame.

LIVE FRAME TIMING SIMULATION
60 Frames / sec ↑ smooth
16 JS thread ms ✓ under budget
§ 02 — Reanimated 3
02

Reanimated 3 — Animations on the UI Thread

The biggest single win is moving animations entirely off the JavaScript thread. Reanimated 3's worklets are small functions that run synchronously on the native UI thread — completely bypassing the JS/Native bridge.

useSharedValue
Holds animated state on the UI thread. No JS involvement during animation.
🧵
worklet functions
JS functions compiled to native bytecode, executed on the UI thread at 60fps.
🎭
useAnimatedStyle
Derives animated styles on UI thread — zero bridge roundtrips.
📦
Gesture Handler v2
Gesture recognition runs native. No async event round-trips.
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  runOnJS
} from 'react-native-reanimated';

function PressCard() {
  const scale = useSharedValue(1);
  const opacity = useSharedValue(1);

  // This function runs ON THE UI THREAD — no bridge cost
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
    opacity: opacity.value,
  }));

  const handlePress = () => {
    scale.value = withSpring(0.96, { damping: 15, stiffness: 300 });
    opacity.value = withSpring(0.8);
    // Jump back to JS only when needed
    runOnJS(onAction)();
  };

  return (
    <Animated.View style={[styles.card, animatedStyle]}>
      <Pressable onPress={handlePress}>
        <Text>Tap me — 60fps on any device</Text>
      </Pressable>
    </Animated.View>
  );
}
💡

Prefix any function you want to run on the UI thread with the 'worklet' directive. Reanimated's Babel plugin handles the compilation — you write JS, it runs native.

§ 03 — FlatList
03

FlatList — Stop Rendering the World

Out of the box, FlatList is conservative with its render window — and that's actually a feature, not a bug. Most teams break it by overriding defaults or forgetting to memoize items. Here's the exact config I use for production lists with 1000+ items.

Scroll Performance Comparison

Default FlatList (no optimizations)28 fps avg
With windowSize + removeClippedSubviews48 fps avg
Full optimization (below config)60 fps avg
import React, { useCallback, memo } from 'react';
import { FlatList, View } from 'react-native';

// 1. Memoize your item — reference equality check prevents re-renders
const ListItem = memo(({ item }) => <ItemRow data={item} />,
  (prev, next) => prev.item.id === next.item.id
);

export function OptimizedList({ data }) {
  // 2. Stable keyExtractor reference
  const keyExtractor = useCallback((item) => item.id, []);

  // 3. Stable renderItem reference — crucial
  const renderItem = useCallback(
    ({ item }) => <ListItem item={item} />,
    []
  );

  return (
    <FlatList
      data={data}
      keyExtractor={keyExtractor}
      renderItem={renderItem}
      // 4. Conservative window — render 5 viewports, not 21
      windowSize={5}
      // 5. Unmount offscreen items from memory
      removeClippedSubviews={true}
      // 6. Pre-render items before they enter viewport
      initialNumToRender={12}
      maxToRenderPerBatch={8}
      updateCellsBatchingPeriod={30}
      // 7. Fixed height items = O(1) scroll position calc
      getItemLayout={(_, index) => ({
        length: ITEM_HEIGHT,
        offset: ITEM_HEIGHT * index,
        index,
      })}
    />
  );
}
⚠️

Skip getItemLayout if your items have variable height — it's better to pay the measure cost than serve wrong scroll positions. Only use it with truly fixed-height rows.

§ 04 — Hermes
04

Hermes Engine — Free Performance

Hermes is enabled by default in React Native 0.70+. If you're on an older project, enabling it is the single highest-ROI change you can make. It compiles JS to bytecode at build time, so the app starts with zero parsing overhead.

Impact on Low-End Devices (Snapdragon 460)

Time-to-Interactive — V8 engine4.2s
Time-to-Interactive — Hermes2.1s
Peak memory — V8186 MB
Peak memory — Hermes130 MB
// android/app/build.gradle
project.ext.react = [
    enableHermes: true,  // ← flip this switch
]

// Verify it's working at runtime:
import { HermesInternal } from 'react-native';
const isHermes = () => !!HermesInternal?.getRuntimeProperties?.();
console.log('Running on Hermes:', isHermes());
§ 05 — Checklist
05

Production Checklist

Before shipping to the Play Store, run through every one of these:

  1. Enable Hermes in build.gradle and verify with HermesInternal check.
  2. Profile with Flipper's Hermes CPU profiler — not the React DevTools profiler — for accurate frame timing.
  3. Replace all Animated.Value usages with Reanimated 3 useSharedValue.
  4. Wrap every FlatList item component in React.memo with a custom comparison function.
  5. Add getItemLayout to any fixed-height list, set windowSize={5}.
  6. Move expensive computations to useMemo; move callbacks to useCallback to stabilize references.
  7. Use InteractionManager.runAfterInteractions to defer heavy work until after navigation transitions.
  8. Enable removeClippedSubviews on Android (optional on iOS — it can cause flicker).
  9. Test on a real low-end Android device (Snapdragon 460 class) — not just simulators.

The cheapest performance win is the one you don't have to write code for. Enabling Hermes takes 30 seconds and gives you 50% TTI improvement.

§ Fin

Final Thoughts

60fps on low-end devices isn't aspirational — it's achievable with the right stack. Reanimated 3 removes the bridge bottleneck for animations. FlatList tuning stops the renderer from thrashing. Hermes eliminates startup overhead. Combined, they transform what a $120 phone can do with your app.

If you're hitting a specific performance wall or want me to profile your app's render tree, reach out. The details are always in the flame chart.