DEV Community

Cover image for React Native’s New Architecture: Sync and async rendering
Megan Lee
Megan Lee

Posted on • Originally published at blog.logrocket.com

React Native’s New Architecture: Sync and async rendering

Written by Emmanuel John✏️

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:

  • Node.js ≥v20 installed
  • Knowledge of React
  • Experience building applications with React Native

What is the New Architecture?

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.

Setting up the New 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:

react native upgrade helper

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:

  1. Open the android/gradle.properties file

  2. Toggle the newArchEnabled flag from true to false

//gradle.properties 
    +newArchEnabled=false 
Enter fullscreen mode Exit fullscreen mode

To opt out from the New Architecture on iOS:

  1. Open the ios/Podfile file
  2. Add ENV['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)
    
  3. Install your CocoaPods dependencies with the command:

    bundle exec pod install
    

To understand async and sync rendering in React Native, you should be familiar with UseLayoutEffect vs. UseEffectin React.

Asynchronous layout and effects

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.

Project setup

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
Enter fullscreen mode Exit fullscreen mode

Run the app

Start the Metro server:

npx react-native start
Enter fullscreen mode Exit fullscreen mode

Open another terminal and run:

npx react-native run-android
Enter fullscreen mode Exit fullscreen mode

or:

npx react-native run-ios
Enter fullscreen mode Exit fullscreen mode

Helper functions

We’ll implement two helper functions to calculate the x and y positions of the tooltip based on:

  • The dimensions and position of the tooltip (toolTip)
  • The target element (target)
  • The boundaries of the root view (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;
}
Enter fullscreen mode Exit fullscreen mode

We’ll also create another helper function for artificial delays:

function wait(ms) {
  const end = Date.now() + ms;
  while (Date.now() < end);
}
Enter fullscreen mode Exit fullscreen mode

Dynamic styling based on position

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 {};
  }
}
Enter fullscreen mode Exit fullscreen mode

ToolTip component

The 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

Targetcomponent

The 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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

We use useCallback to get the measurements of the view and then update the positioning of the tooltip based on where the view is.

Demo component

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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:

Demo Component

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.

Synchronous layout and effects

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](https://react.dev/reference/react/useLayoutEffect) Hook to measure and apply layout updates synchronously in a single commit, avoiding the visual "jump."

ToolTip component

This 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 component

This 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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 component

This 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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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:

react native sync render demo component

The New Architecture performance benchmarks

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
Enter fullscreen mode Exit fullscreen mode

Then, install dependencies:

cd RNNewArchitectureApp/App
yarn install
Enter fullscreen mode Exit fullscreen mode

Run the following command to configure the project to use this New Architecture:

RCT_NEW_ARCH_ENABLED=1 npx pod-install
Enter fullscreen mode Exit fullscreen mode

Navigate to the ios directory:

cd ios
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You can also run yarn android to build the app in debug mode.

Here’s what your running app should look like:

Old Architecture Running App

Old Architecture Time For Running

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:

Virtual emulator: Google Pixel 5 API 33

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

Conclusion

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: Instantly recreate issues in your React Native apps

Instantly recreate issues in your React Native apps

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.

Top comments (0)