React Native just received a significant performance boost with the release of its New Architecture.
The New Architecture, which is now the default for new installations, addresses long-standing complaints about speed and efficiency. If you’re on React Native 0.76 or the latest version, these enhancements are already available, making it an exciting time to explore what this means for your projects.
React Native’s New Architecture ships with better performance, improved developer experience, and alignment with React’s modern features.
This article will explore practical use cases for synchronous and asynchronous rendering with the New Architecture. We’ll also create performance benchmarks to compare the old and New Architectures.
Below are a few prerequisites that you’ll need before moving forward with this article:
The New Architecture is a redesign of React Native’s internal systems to address the challenges encountered in the legacy architecture. It supports both asynchronous and synchronous updates.
Traditionally, React Native relied on a bridge to connect JavaScript and native code. While this approach worked well, it introduced overhead. Now, the New Architecture removes the asynchronous bridge between JavaScript and native, replacing it with the JavaScript Interface (JSI). It can directly call native C, C++, or Kotlin code (on Android) without the need for bridging. This allows for shared memory between JavaScript and native layers, significantly improving performance.
When paired with technologies like static Hermes, which compiles JavaScript to assembly, React Native enables the creation of incredibly fast apps.
One of the common issues with the old architecture is the visibility of intermediate states or visual jumps between rendering the initial layout and further updates to the layouts.
The key changes in the New Architecture include synchronous layout updates, concurrent rendering, JavaScript Interface (JSI), and support for advanced React 18+ features like suspense transitions, automatic batching, and useLayoutEffect
.
It also enables backward compatibility with libraries targeting the old architecture.
React Native 0.76 or the latest version ships with the New Architecture by default. If you use Expo, React Native 0.76 is now supported in Expo SDK 52.
If you need to introduce the New Architecture in a legacy codebase, React Native Upgrade Helper is a helpful tool that makes it easy to migrate your React Native codebase from one version to another:
All you need to do is enter your current react-native
version and the version you would like to upgrade to. Then you’ll see the necessary changes you should make to your codebase.
To opt out from the New Architecture on Android:
Open the android/gradle.properties
file
Toggle the newArchEnabled
flag from true
to false
//gradle.properties +newArchEnabled=false
To opt out from the New Architecture on iOS:
ios/Podfile
fileENV['RCT_NEW_ARCH_ENABLED'] = '0'
in the main scope of the Podfile (reference Podfile in the template):
+ ENV['RCT_NEW_ARCH_ENABLED']= '0' require Pod::Executable.execute_command('node', ['-p',
'require.resolve)
bundle exec pod install
To understand async and sync rendering in React Native, you should be familiar with UseLayoutEffect
vs. UseEffect
in React.
One of the most common issues with the legacy architecture was the visual glitches during layout changes. This is because developers needed to use the asynchronous onLayout
event to read layout information of a view (which was also asynchronous). This caused at least one frame to render an incorrect layout before it could be read and updated.
The New Architecture solves this issue by allowing synchronous access to layout information and ensuring properly scheduled updates. This way, users never see any intermediate state.
To experience the improvements in performance and user experience provided by the New Architecture, we’ll build an adaptive tooltip using the legacy architecture to experience the visual glitches.
In the next section, we’ll build the same using the New Architecture. You’ll see that the tooltip will align perfectly without intermediate state jumps, which solves the visual glitches issue that causes a poor user experience.
Ensure you have a React Native environment configured. Check out the React Native CLI Quickstart guide if you haven’t done this.
Run the following in your project folder:
npx react-native init ToolTipApp cd ToolTipApp
Start the Metro server:
npx react-native start
Open another terminal and run:
npx react-native run-android
or:
npx react-native run-ios
We’ll implement two helper functions to calculate the x and y positions of the tooltip based on:
(toolTip
)target
)rootView
)In the src
directory, create a utils
folder. Inside it, add a new file named helper.js
and include the following code:
export function calculateX(toolTip, target, rootView) { let toolTipX = target.x + target.width / 2 - toolTip.width / 2; if (toolTipX < rootView.x) { toolTipX = target.x; } if (toolTipX + toolTip.width > rootView.x + rootView.width) { toolTipX = rootView.x + rootView.width - toolTip.width; } return toolTipX - rootView.x; } export function calculateY(toolTip, target, rootView) { let toolTipY = target.y - toolTip.height; if (toolTipY < rootView.y) { toolTipY = target.y + target.height; } return toolTipY - rootView.y; }
We’ll also create another helper function for artificial delays:
function wait(ms) { const end = Date.now() + ms; while (Date.now() < end); }
We’ll create another helper function getStyle
which returns the appropriate alignment styles for each tooltip position:
function getStyle(position) { switch (position) { case 'top-left': return { justifyContent: 'flex-start', alignItems: 'flex-start' }; case 'center-center': return { justifyContent: 'center', alignItems: 'center' }; case 'bottom-right': return { justifyContent: 'flex-end', alignItems: 'flex-end' }; default: return {}; } }
ToolTip
componentThe ToolTip
component measures its dimensions (rect
) asynchronously after layout and dynamically updates its position using the calculateX
and calculateY
functions.
In the src
directory, create a components
folder. Inside it, add a new file named ToolTip.jsx
and include the following code:
import * as React from 'react'; import {View} from 'react-native'; import {calculateX, calculateY} from '../utils/helper' function ToolTip({ position, targetRect, rootRect, children }) { const ref = React.useRef(null); const [rect, setRect] = React.useState(null); const onLayout = React.useCallback(() => { ref.current?.measureInWindow((x, y, width, height) => { setRect({ x, y, width, height }); }); }, []); let left = 0; let top = 0; if (rect && targetRect && rootRect) { left = calculateX(rect, targetRect, rootRect); top = calculateY(rect, targetRect, rootRect); } return ( <View ref={ref} onLayout={onLayout} style={{ position: 'absolute', borderColor: 'green', borderWidth: 2, borderRadius: 8, padding: 4, top, left, }} > {children} </View> ); }
We use a ref
to store a reference to the View
element, allowing us to measure its dimensions and position on the screen. The onLayout
callback is triggered whenever the layout of the View
changes. Within this callback, the measureInWindow
method retrieves the tooltip’s x
, y
, width
, and height
, which are then stored in the rect
state.
Target
componentThe Target
component measures its dimensions and passes them to the ToolTip
component.
In the components
directory, add a new file named Target.jsx
and include the following code:
import * as React from 'react'; import {Pressable, Text, View} from 'react-native'; import ToolTip from './ToolTip' function Target({ toolTipText, targetText, position, rootRect }) { const targetRef = React.useRef(null); const [rect, setRect] = React.useState(null); const onLayout = React.useCallback(() => { targetRef.current?.measureInWindow((x, y, width, height) => { setRect({ x, y, width, height }); }); }, []); return ( <> <View ref={targetRef} onLayout={onLayout} style={{ borderColor: 'red', borderWidth: 2, padding: 10, }} > <Text>{targetText}</Text> </View> <ToolTip position={position} rootRect={rootRect} targetRect={rect}> <Text>{toolTipText}</Text> </ToolTip> </> ); }
We use useCallback
to get the measurements of the view and then update the positioning of the tooltip based on where the view is.
This component dynamically updates the position of a Target
component’s tooltip every second, rotates through different tooltip positions, and measures the root view dimensions to calculate relative tooltip positions.
In the components
directory, add a new file named Demo.jsx
and include the following code:
import * as React from 'react'; import {Text, View} from 'react-native'; import Target from './Target' export function Demo() { const positions = ['top-left', 'top-right', 'center-center', 'bottom-left', 'bottom-right']; const [index, setIndex] = React.useState(0); const [rect, setRect] = React.useState(null); const ref = React.useRef(null); React.useEffect(() => { const interval = setInterval(() => { setIndex((prevIndex) => (prevIndex + 1) % positions.length); }, 1000); return () => clearInterval(interval); }, []); const onLayout = React.useCallback(() => { ref.current?.measureInWindow((x, y, width, height) => { setRect({ x, y, width, height }); }); }, []); const position = positions[index]; const style = getStyle(position); return ( <> <Text style={{ margin: 20 }}>Position: {position}</Text> <View ref={ref} onLayout={onLayout} style={{ ...style, flex: 1, borderWidth: 1 }}> <Target toolTipText="This is the tooltip" targetText="This is the target" position={position} rootRect={rect} /> </View> </> ); }
In the useEffect
Hook, we set up an interval to increment the position index every second, resetting it when it reaches the end of the array. We also attached a ref
to the root View
container and used the measureInWindow
method in the onLayout
callback to capture the x
, y
, width
, and height
of the root container. This information is stored in the rect
state and passed to the Target
component, enabling it to position its tooltip relative to the root container.
Here is what your demo component should look like:
Notice the time difference between the tooltip’s movement and the target component. That’s the visual glitch. For a better user experience, both components should move together at the same time.
We can avoid visual glitch issues completely with synchronous access to layout information and properly scheduled updates, such that no intermediate state is visible to users.
With the New Architecture, we can use useLayoutEffect
Hook to measure and apply layout updates synchronously in a single commit, avoiding the visual “jump.”
ToolTip
componentThis component dynamically positions the tooltip based on targetRect
, rootRect
, and its own dimensions:
export function ToolTip({position, targetRect, rootRect, children}) { const ref = React.useRef(null); const [rect, setRect] = React.useState(null); React.useLayoutEffect(() => { wait(200); // Simulate delay setRect(ref.current?.getBoundingClientRect()); }, [setRect, position]); let left = 0, top = 0; if (rect && targetRect && rootRect) { left = calculateX(rect, targetRect, rootRect); top = calculateY(rect, targetRect, rootRect); } return ( <View ref={ref} style={{ position: 'absolute', borderColor: 'green', borderRadius: 8, borderWidth: 2, padding: 4, top, left, }}> {children} </View> ); }
In the useLayoutEffect
Hook, we simulate a delay (using the wait
function) and then update the tooltip’s position by calling getBoundingClientRect()
on the referenced View
element. This information is stored in the rect
state and used to calculate the position of the tooltip relative to the target element and the root container using calculateX
and calculateY
functions.
Target
componentThis represents the target element and renders the tooltip relative to itself. It calculates its dimensions using getBoundingClientRect
.
function Target({toolTipText, targetText, position, rootRect}) { const targetRef = React.useRef(null); const [rect, setRect] = React.useState(null); React.useLayoutEffect(() => { wait(200); // Simulate delay setRect(targetRef.current?.getBoundingClientRect()); }, [setRect, position]); return ( <> <View ref={targetRef} style={{ borderColor: 'red', borderWidth: 2, padding: 10, }}> <Text>{targetText}</Text> </View> <ToolTip position={position} rootRect={rootRect} targetRect={rect}> <Text>{toolTipText}</Text> </ToolTip> </> ); }
We use useRef
to create a reference to the target element (targetRef
), and useState
to store its dimensions and position (rect
). In the useLayoutEffect
Hook, we simulate a delay using the wait
function, then update the rect
state by calling getBoundingClientRect()
on the target element to capture its position and size.
Demo
componentThis component demonstrates dynamic repositioning of the tooltip by cycling through predefined positions every second:
function Demo() { const toolTipText = 'This is the tooltip'; const targetText = 'This is the target'; const ref = React.useRef(null); const [index, setIndex] = React.useState(0); const [rect, setRect] = React.useState(null); React.useEffect(() => { const setPosition = setInterval(() => { setIndex((index + 1) % positions.length); // Cycle positions }, 1000); return () => clearInterval(setPosition); }, [index]); const position = positions[index]; const style = getStyle(position); React.useLayoutEffect(() => { wait(200); setRect(ref.current?.getBoundingClientRect()); }, [setRect, position]); return ( <> <Text style={{margin: 20}}>Position: {position}</Text> <View style={{...style, flex: 1, borderWidth: 1}} ref={ref}> <Target toolTipText={toolTipText} targetText={targetText} rootRect={rect} position={position} /> </View> </> ); }
We initialize a state variable index
to track the current position, which cycles through the positions
array every second using setInterval
within a useEffect
Hook. The position
is updated and used to compute the layout style for the root View
container using the getStyle
function.
The useLayoutEffect
Hook is used to capture the dimensions and position of the root container (ref
) after a simulated delay, storing the information in the rect
state. This rect
is then passed to the Target
component to position the tooltip relative to the root container.
Here is what your demo component should look like:
The React Native team has created an app that combines various performance scenarios into one place. This app makes it easier to compare the old and New Architecture and identify any performance gaps in the New Architecture.
In this section, we’ll build and run benchmarks to evaluate the performance differences between the old and New Architecture.
To begin, run the following command to clone the app:
git clone --branch new-architecture-benchmarks https://github.com/react-native-community/RNNewArchitectureApp
Then, install dependencies:
cd RNNewArchitectureApp/App yarn install
Run the following command to configure the project to use this New Architecture:
RCT_NEW_ARCH_ENABLED=1 npx pod-install
Navigate to the ios
directory:
cd ios
Open MeasurePerformance.xcworkspace
. Press CMD + I
for an optimized build or CMD + R
for a debug build.
For Android, run the following command to build the app with optimizations:
yarn android --mode release
You can also run yarn android
to build the app in debug mode.
Here’s what your running app should look like:
Click each button to see how long it takes to render the corresponding components.
Next, switch to the New Architecture tab, repeat the process, and compare the results.
Below is a comparison of my results:
Scenario | Old Architecture | New Architecture | Difference |
---|---|---|---|
1500 | 282ms | 252ms | New Architecture is ~8% faster |
5000 | 1088ms | 1035ms | New Architecture is ~4% faster |
1500 | 512ms | 503ms | New Architecture is ~1% faster |
5000 | 2156ms | 2083ms | New Architecture is ~3% faster |
1500 | 406ms | 402ms | New Architecture is neutral with old architecture |
5000 | 1414ms | 1378ms | New Architecture is ~3% faster |
In this article, we explored synchronous and asynchronous rendering in React Native through practical use cases and compared the performance of the old and New Architecture. With the benchmark results, we can see the significant advantages of adopting this New Architecture. If you’re using React Native 0.76 or later, the New Architecture is already supported and works out of the box, requiring no additional configuration.
LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]