Using RCTView & RCTText in React Native for Performance Gains
Mo Khazali6 min read
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.
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:
- 1000, 2000, & 3000 Empty Views with a border on each.
- The same number of views with a single text node added into each.
We run these tests to test the three following cases:
- Regular
View
andText
elements, running on the old architecture (baseline) - Native
View
andText
elements, running on the old architecture - Native
View
andText
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
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
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 View
s 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. 🙂