understanding bridge, JSI and the new architecture
Janky lists, delayed touch reactions and sluggish animations in React Native almost always share the same cause: too much asynchronous communication between JavaScript and the native side. The new architecture with JSI and Fabric has structurally removed many of these bottlenecks, yet performance remains a question of using the right techniques in your own code.
Table of Contents
- 1. Why React Native performance was historically a bridge problem
- 2. The old bridge architecture in detail
- 3. JSI: direct access instead of serialized messages
- 4. Fabric: the new renderer for synchronous layouts
- 5. TurboModules: lazy loading native modules
- 6. List performance: configuring FlatList correctly
- 7. Avoiding re-renders: memoization in practice
- 8. Measuring performance instead of guessing
- 9. Old architecture and new architecture compared
- 10. Summary
- 11. FAQ
1. Why React Native performance was historically a bridge problem
Anyone dealing with React Native performance inevitably runs into the term bridge, since the original architecture of React Native was the most important limiting factor for smooth apps. JavaScript ran in its own thread, the native side with UI rendering in another, and every communication between them had to go through asynchronous, JSON serialized messages. This architecture worked well for occasional interactions but became a bottleneck as soon as many messages per second were needed, for instance for scroll events, gestures or animations.
The core problem for React Native performance was the serialization itself. Every call to a native method had to be converted into a JSON string, sent over the bridge and parsed again on the other side. For simple, infrequent calls, this overhead was unnoticeable. For complex animations running at 60 frames per second, where every frame had to send several position and transform values over the bridge, the overhead added up to noticeable jank, meaning visible stuttering in the user interface.
Meta addressed exactly this structural problem with the so called new architecture, consisting of JSI, Fabric and TurboModules. These three components replace the bridge with more direct communication paths and have been the standard for new projects since React Native 0.76. For React Native performance, that means many earlier best practices around avoiding the bridge are less critical today, while new techniques have become relevant.
2. The old bridge architecture in detail
To understand React Native performance problems, a closer look at the classic bridge helps. JavaScript code that, for instance, wanted to pass StyleSheet.create() values to a native view created a message object that was queued, batch serialized and asynchronously transmitted to the native side. This asynchrony meant JavaScript could never synchronously wait for a native value without using callbacks or promises, which made certain patterns, for instance synchronously reading a layout dimension right after rendering, impossible or at least error prone.
The second effect concerned memory consumption: every bridge message temporarily had to exist as a complete JSON object in memory, even for simple numeric values. At high message frequency, for instance during fast scrolling with simultaneous callback calls, garbage collection load could increase noticeably and cause additional frame drops, independent of the application's actual JavaScript code.
3. JSI: direct access instead of serialized messages
The JavaScript Interface, JSI for short, is the foundation of the new React Native architecture and the most important lever for better React Native performance. Instead of serializing messages and sending them asynchronously, JSI allows JavaScript to hold direct references to native C++ objects and call their methods synchronously, entirely without a detour through a message queue. This direct binding eliminates serialization costs for the vast majority of interactions between JavaScript and native code.
For developers, this difference mainly shows up indirectly: libraries such as Reanimated 3 use JSI to compute animation values directly on the UI thread, without needing to involve the JavaScript thread per frame. The result is animations that stay smooth at 60 or even 120 frames per second even when the JavaScript thread is currently busy with other tasks, for instance processing a network response. This effect was structurally impossible under the old bridge architecture, because every communication necessarily had to run through the JavaScript thread.
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
import { Pressable } from 'react-native';
// Runs entirely on the UI thread via JSI, no bridge round trip per frame
function LikeButton() {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const handlePress = () => {
scale.value = withSpring(1.3, {}, () => {
scale.value = withSpring(1);
});
};
return (
<Pressable onPress={handlePress}>
<Animated.View style={animatedStyle}>
{/* Icon content */}
</Animated.View>
</Pressable>
);
}
4. Fabric: the new renderer for synchronous layouts
Fabric is the new rendering layer of React Native that ties shadow tree computation and the actual native view creation more closely to React itself, delivering significant React Native performance gains for complex layouts. Unlike the old renderer, Fabric allows synchronous layout measurements, meaning a component can query its actual dimensions immediately after rendering, without waiting for an asynchronous callback over the bridge.
Another practical advantage of Fabric concerns competing updates: React 18 and React 19 support concurrent features such as useTransition and priority levels for updates, and Fabric actually enforces this prioritization down into the native rendering layer. A high priority update, for instance a text input, can therefore interrupt lower priority work, for instance rendering a long list in the background, resulting in noticeably more responsive input fields, even while the app is busy with a lot of background rendering work.
5. TurboModules: lazy loading native modules
TurboModules replace the old native modules system and improve React Native performance primarily at app startup time. Under the old architecture, all registered native modules were initialized at app start, regardless of whether the app actually used them in that session. For apps with many dependencies, for instance payment providers, analytics SDKs and map libraries, this initialization added up to noticeable delays during cold start.
TurboModules load native modules lazily instead, only upon actual first access from JavaScript. A payment module that is only needed at checkout therefore does not initialize at app start, but only at the moment the user actually proceeds to checkout. For apps with many rarely used native dependencies, this difference can shorten perceived startup time by several hundred milliseconds, a direct, measurable win for React Native performance without any change to your own application code.
6. List performance: configuring FlatList correctly
Long lists are the classic stress test for React Native performance, and FlatList offers several configuration options for this that in practice often go unused. getItemLayout tells the list the exact dimensions of every entry in advance, so FlatList does not need to perform expensive layout recalculation while scrolling. This optimization is particularly effective for lists with a uniform row height, for instance chat messages or fixed size product cards.
windowSize, maxToRenderPerBatch and removeClippedSubviews together control how many entries outside the visible area get pre rendered, and how aggressively non visible native views get removed. Too generous a configuration of these values wastes memory and computation time on entries the user never sees, while too tight a configuration makes empty white areas visible during fast scrolling before content loads. The right balance depends heavily on the complexity of individual rows and should always be measured against your own dataset.
import { FlatList } from 'react-native';
const ITEM_HEIGHT = 72;
// getItemLayout skips expensive layout measurement during scroll
function OrderHistoryList({ orders }) {
return (
<FlatList
data={orders}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <OrderRow order={item} />}
getItemLayout={(_, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
windowSize={5}
maxToRenderPerBatch={10}
removeClippedSubviews
initialNumToRender={8}
/>
);
}
7. Avoiding re-renders: memoization in practice
Unnecessary re-renders remain one of the most common causes of poor React Native performance even under the new architecture, because every re-render of a component still costs JavaScript computation time, regardless of how efficient the bridge communication has become in the meantime. React.memo for list items prevents a single row from re-rendering when only the parent container's state changes, while the row's own props stay identical.
useCallback for functions passed as props to memoized child components prevents a new function reference from being created on every render, which would otherwise render React.memo ineffective. The React Compiler, if already enabled in the project, takes over much of this manual memoization automatically, but does not reduce the need to structure your own state's data shape so that unnecessary object identity changes do not occur in the first place.
8. Measuring performance instead of guessing
Every optimization for React Native performance should start with a measurement, not a guess. The React DevTools Profiler shows which components re-render how often and with what render duration, frequently uncovering surprising hotspots, for instance a context provider component that causes unnecessary re-renders of many child components through a frequently changing value. In addition, the native Flipper, or the newer React Native DevTools Profiler, provides insight into native rendering times, JavaScript thread load and memory consumption.
For production apps, continuous performance monitoring through services such as Sentry or Firebase Performance is worthwhile, since developer devices are often significantly more powerful than the devices actually used by the user base in the field. A feature that runs smoothly on a current flagship smartphone can stutter noticeably on a three year old mid range device, a difference that often goes undetected without real user metrics.
9. Old architecture and new architecture compared
The following table contrasts the central React Native performance relevant differences between the old bridge architecture and the new architecture with JSI, Fabric and TurboModules.
| Area | Old Architecture (Bridge) | New Architecture | Performance effect |
|---|---|---|---|
| JS to native communication | Asynchronous, JSON serialized | Synchronous via JSI | No serialization overhead |
| Layout measurement | Asynchronous callback | Synchronous via Fabric | Immediate dimension query |
| Module initialization | All at startup | Lazy via TurboModules | Faster cold start |
| Animations in the background | JS thread blocks animation | UI thread via JSI/Reanimated | Smooth despite JS load |
| Update prioritization | Not supported | Concurrent features via Fabric | More responsive inputs |
The table clearly shows that the new architecture solves structural bottlenecks that used to exist regardless of an application's own code quality. Still, list configuration, memoization and profiling remain the development team's responsibility, since no architecture automatically makes poorly structured application code performant.
Mironsoft
React Native performance audits and optimization
Janky lists and sluggish animations in your app?
We profile your React Native app on real devices, identify the actual bottlenecks and implement targeted optimizations, from list configuration to migrating to the new architecture.
Performance audit
Profiling on real devices with React Native DevTools and Flipper
Architecture migration
Moving to JSI, Fabric and TurboModules without feature regressions
List optimization
FlatList configuration and memoization for smooth scrolling
10. Summary
The fundamentals of good React Native performance have changed structurally with the new architecture. JSI replaces the asynchronous, JSON serialized bridge with direct, synchronous access to native objects. Fabric brings synchronous layout measurement and real update prioritization into the rendering layer. TurboModules load native dependencies lazily, noticeably shortening cold start time. Together, these three components solve many bottlenecks that used to exist under the old architecture regardless of your own application code's quality.
Still, React Native performance is not purely an architecture question: FlatList configuration with getItemLayout, consistent memoization with React.memo and useCallback, and continuous profiling with React DevTools remain mandatory tasks for every team. Anyone who measures before optimizing avoids wasting time on places that are not actually relevant to real user experience.
React Native Performance Fundamentals, the key points at a glance
JSI
Synchronous, direct access to native objects instead of serialized bridge messages.
Fabric
New renderer with synchronous layout measurement and real update prioritization.
TurboModules
Lazy loading of native modules noticeably shortens cold start time.
Lists and memoization
getItemLayout, React.memo and useCallback remain mandatory regardless of the architecture.