React NativePerformanceMobileiOSAndroid

React Native Cross-Platform Performance: Bridging the Gap Between iOS and Android

Practical techniques for reducing JS thread work, eliminating bridge serialisation overhead, and achieving 60 fps on mid-range Android devices.

Salem Shah··8 min read

React Native Cross-Platform Performance: Bridging the Gap Between iOS and Android

React Native's JavaScript-to-native bridge is the single biggest source of performance problems in production apps. Understanding exactly how it works — and how the New Architecture eliminates it — is the difference between a 60 fps app and a janky one.

The Old Architecture: Bridge Serialisation

In the classic React Native architecture, every interaction between JavaScript and native code (layout, gestures, animations) crosses an asynchronous bridge. The data is JSON-serialised, transferred, and deserialised on the other side.

JS Thread  →  [JSON serialize]  →  Bridge  →  [JSON deserialize]  →  Native Thread

This round-trip introduces latency measured in milliseconds. For animations tied to user gestures, milliseconds become visible jank.

The New Architecture: JSI

The JavaScript Interface (JSI) allows JavaScript to hold direct references to C++ objects. There is no serialisation. The call is synchronous.

// Example: calling a native module with JSI
import { NativeModules } from "react-native";
const result = NativeModules.Camera.captureFrame(); // synchronous

JSI enables three major features:

  1. Fabric — the new concurrent renderer (same model as React 18's concurrent features)
  2. TurboModules — lazy-loaded native modules, only initialised when first called
  3. Codegen — static type checking between JS and native at build time

Measuring Performance Before Optimising

Never optimise blind. Use these tools first:

| Tool | What it measures | |---|---| | Flipper Performance Plugin | JS thread, UI thread, render times | | React DevTools Profiler | Component render counts and duration | | Android Systrace | Native thread interaction | | Xcode Instruments | iOS CPU and memory | | InteractionManager.runAfterInteractions | Task scheduling impact |

The Five Most Common Performance Mistakes

1. Inline Styles in Lists

// Bad — creates a new object on every render
<View style={{ paddingHorizontal: 16, backgroundColor: "#fff" }}>

// Good — StyleSheet.create memoises the style object
const styles = StyleSheet.create({ container: { paddingHorizontal: 16 } });
<View style={styles.container}>

2. Unmemoised List Item Components

Every FlatList item should be wrapped in React.memo. Without it, scrolling re-renders every visible item on every parent state change.

const ListItem = React.memo(({ item }: { item: Item }) => {
  return <View>...</View>;
});

3. Running Animations on the JS Thread

Animated.Value with useNativeDriver: false schedules animation frames on the JS thread. Every other JS operation can delay the next frame.

// Always use native driver for transform/opacity animations
Animated.spring(opacity, { toValue: 1, useNativeDriver: true }).start();

4. Large Bundle Size

Every byte of your JS bundle must be parsed before the app becomes interactive. Audit your bundle with react-native-bundle-visualizer.

Typical wins:

  • Replace moment.js (330 kB) with day.js (6 kB)
  • Import only the icons you use from icon libraries
  • Enable Hermes on Android (bytecode pre-compilation)

5. Missing keyExtractor in FlatList

Without a stable key, React Native recreates DOM nodes instead of recycling them. The key must be unique and stable across renders.

<FlatList
  data={items}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <ListItem item={item} />}
/>

Scaling Strategy for Large Apps

  1. Code splitting by screen — Metro bundler can create per-screen bundles with ram-bundles format
  2. Image caching — use react-native-fast-image instead of the built-in Image component
  3. SQLite for offline dataop-sqlite (C++ SQLite via JSI) is 5x faster than AsyncStorage for structured data
  4. Reanimated 3 for gesture-driven animations — runs entirely on the UI thread, zero JS involvement

iOS vs Android: The Real Differences

| Concern | iOS | Android | |---|---|---| | JS engine | JavaScriptCore | Hermes (recommended) | | Animation budget | 16.7 ms (60 fps) | 16.7 ms, but more GC pauses | | Memory | More lenient | Aggressive process killing | | Bundle format | Regular JS | RAM bundles or Hermes bytecode |

Lessons Learned

  • Profile on a mid-range Android device (not a flagship). That is your real user.
  • The New Architecture is production-ready since React Native 0.73. Migrate when you can.
  • Most performance regressions come from list item complexity, not from any single computation.
  • useNativeDriver: true on every animation is non-negotiable for 60 fps.

Working on a React Native app and hitting a performance wall? I am available for consulting — reach out at salemshahdev@gmail.com.