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:
- Fabric — the new concurrent renderer (same model as React 18's concurrent features)
- TurboModules — lazy-loaded native modules, only initialised when first called
- 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
- Code splitting by screen — Metro bundler can create per-screen bundles with
ram-bundlesformat - Image caching — use
react-native-fast-imageinstead of the built-inImagecomponent - SQLite for offline data —
op-sqlite(C++ SQLite via JSI) is 5x faster than AsyncStorage for structured data - 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: trueon 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.