Skip to content
Logo Theodo

Using RCTView & RCTText in React Native for Performance Gains

Mo Khazali6 min read

A React Native logo flying on a rocket.

I recently published an article and thread comparing iOS rendering performance across SwiftUI, React Native, and Flutter. The results showed that SwiftUI (unsurprisingly) performs the best, followed by React Native and Flutter respectively.

I got some interesting feedback and suggestions from the React Native community. Nate Birdman suggested that replacing View elements with the ViewNativeComponent can boost performance greatly. Separately, I saw on a reposted thread that William Candillon had mentioned that using the equivalent native text element also improves performance.

This piqued my curiosity and I wanted to investigate a bit further.

An Accurate Method of Measuring Render Times

After I posted my original article, members of the React & React Native core team mentioned that the original method of measuring wasn’t actually accurate. The original method would store a timestamp in a useState and would measure the difference between when a useLayoutEffect would fire compared to the original timestamp.

A timeline showing the paint time vs when useLayoutEffect is called

This approach isn’t accurate, because useLayoutEffect is run on the JS thread, and isn’t synchronised with when the paint happens on the UI thread. This means that it can be called either before or after the paint being completed on the native layer. As a result, timings measured with useLayoutEffect can either be over or under reported.

Instead, to get accurate measurements of how long the full rendering cycle (including the paint) takes, we’ll need to drop into the native layer. Samuel Susla has a great repo which was used to compare Old & New architecture performance, and I was able to use it to run my experiments with some minor modifications.

This repo defines a turbomodule called RTNTimeToRender with a native component that can store start and end times for a “marker” on the native layer. The start time is gotten from the timestamp of the touch ended event (which comes from the native layer), and on each platform, the end time of the render is calculated using the respective host platform’s event marking the view becoming visible - on iOS, this is the didMoveToWindow method, and on Android, it is the onDraw method.

The Experiment

We’re running similar experiments as the previous tests. We render a large number of View and Text elements, and measuring how long the paint took in each instance. These are run on a bare RN project with no added dependencies to avoid any potential added overhead.

We run the following set of experiments 10 times for each test case and average out the results:

  1. 1000, 2000, & 3000 Empty Views with a border on each.
  2. The same number of views with a single text node added into each.

We run these tests to test the three following cases:

  1. Regular View and Text elements, running on the old architecture (baseline)
  2. Native View and Text elements, running on the old architecture
  3. Native View and Text elements, running with Fabric

Each of these tests were run on a 2021 M1 MacBook Pro with 16GB of memory, running an iPhone 14 Simulator with iOS 17.

The Results

iOS

Graph of Average Rendering Performance on iOS

Test Baseline, Old Arch Native Views, Old Arch Native Views, New Arch
Average (ms) SD Average (ms) SD Average (ms) SD
1000 Views 104.00 15.00 103.80 10.33 110.60 3.27
2000 Views 205.50 1.90 181.70 10.52 176.80 9.80
3000 Views 287.5 3.03 238.9 10.58 249.5 6.75
1000 Views w/ Text 277.2 9.86 225.8 1.99 189.5 3.78
2000 Views w/ Text 536.5 29.49 414.00 11.85 343.7 7.01
3000 Views w/ Text 765.40 5.46 603.40 5.72 504.50 8.77

Android

Graph of Average Rendering Performance on Android

Test Baseline, Old Arch Native Views, Old Arch Native Views, New Arch
Average (ms) SD Average (ms) SD Average (ms) SD
1000 Views 124.40 31.69 118.20 49.90 87.30 12.51
2000 Views 175.70 11.49 167.20 19.05 150.00 21.53
3000 Views 248.90 25.13 189.20 11.91 181.50 27.27
1000 Views w/ Text 218.80 32.95 186.60 14.55 198.40 26.70
2000 Views w/ Text 382.50 25.76 303.70 27.89 340.60 37.80
3000 Views w/ Text 542.80 68.51 425.00 50.19 448.50 37.30

Learnings

On average across all of our experiments, we get around a 15 percent improvement in rendering times on both Android and iOS. Interestingly, on iOS the standard deviation is quite low (an average of ~8.6ms), whereas we see a much larger deviation on Android (average of ~29.6ms).

Drilling in a little bit more, on iOS, we start to see larger performance improvements when rendering both NativeViews and NativeTexts, especially with the larger numbers of view & text nodes. Similarly on Android, we found the biggest jumps in performance when rendering 2000 & 3000 views with Text nodes (around ~20% better).

When we switch to the new architecture on iOS, we found that performance was very similar when testing just views. However, there were quite significant improvements on rendering times with text nodes introduced (around ~16% across the board). Surprisingly, the results on Android were the opposite - rendering View & Text elements were between 5-12% slower with Fabric. These results match the performance tests that the RN team released, which showed that there were marginally improvements with Views on Android, but not much of a difference with Text nodes. They attributed this to Android being slow at measuring text.

Why do NativeViews & NativeTexts improve render times?

The JS View & Text components add an extra level of depth to the rendering tree. These elements take the props, transforms some of them, and passes them to the inner native views and texts (RCTView and RCTText). In the Text element, an onPress prop is added, which isn’t commonly used (since you’d use a Pressable or TouchableOpacity).

It seems like rendering depth has a large effect on render times, and this approach removes one level of depth, across the board, which, as we’ve seen, can have a large improvement on the rendering times.

Feel free to reach out

Feel free to reach out to me on Twitter @mo__javad. 🙂

Liked this article?