From d009a02c6c4adc78f0f009f75e2fe9927a274bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Wed, 13 Aug 2025 10:14:21 -0700 Subject: [PATCH] Add benchmark to compare rendering times for View and ViewNativeComponent (#53248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/53248 Changelog: [internal] This adds a new benchmark that compares the rendering time (only rendering, not committing, mounting, effects, etc.) of `` and ``. Baseline: | (index) | Task name | Latency avg (ns) | Latency med (ns) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples | | ------- | ---------------------------------------- | ----------------- | ------------------ | ---------------------- | ---------------------- | ------- | | 0 | 'render 100 views (Noop)' | '333036 ± 0.39%' | '328452 ± 2393.0' | '3019 ± 0.18%' | '3045 ± 22' | 3003 | | 1 | 'render 100 views (ViewNativeComponent)' | '1335974 ± 3.45%' | '1228468 ± 7541.5' | '797 ± 0.71%' | '814 ± 5' | 1000 | | 2 | 'render 100 views (View)' | '2296988 ± 1.60%' | '2170821 ± 12374' | '449 ± 0.74%' | '461 ± 3' | 1000 | This shows that **`` currently has an overhead of 75% in rendering time**. I've also tested a modification of `View` such as: ``` component View(...props: ViewProps) { return { // This tag allows us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE, // Built-in properties that belong on the element type: ViewNativeComponent, key: undefined, // $FlowExpectedError[prop-missing] ref: props.ref, props, }; } ``` This makes `View` basically a no-op component, and the benchmark after this looks like: | (index) | Task name | Latency avg (ns) | Latency med (ns) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples | | ------- | ---------------------------------------- | ----------------- | ----------------- | ---------------------- | ---------------------- | ------- | | 0 | 'render 100 views (View)' | '1743010 ± 2.25%' | '1630816 ± 10616' | '600 ± 0.74%' | '613 ± 4' | 1000 | | 1 | 'render 100 views (ViewNativeComponent)' | '1370699 ± 4.04%' | '1242284 ± 14172' | '789 ± 0.74%' | '805 ± 9' | 1000 | This shows that `View`, just for existing as a wrapper component, has an overhead of 31% in rendering time, which means that **the opportunities to reduce the overhead beyond what we already did are limited**. Reviewed By: rshest Differential Revision: D80169514 fbshipit-source-id: aa2a1fc3f9d0ee3a60c03dd32555802fa7265251 --- ...-vs-ViewNativeComponent-benchmark-itest.js | 60 +++++++++++++++++++ .../__tests__/utilities/measureRenderTime.js | 46 ++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 packages/react-native/Libraries/Components/View/__tests__/View-vs-ViewNativeComponent-benchmark-itest.js create mode 100644 packages/react-native/src/private/__tests__/utilities/measureRenderTime.js diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-vs-ViewNativeComponent-benchmark-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-vs-ViewNativeComponent-benchmark-itest.js new file mode 100644 index 00000000000..1d9370309d9 --- /dev/null +++ b/packages/react-native/Libraries/Components/View/__tests__/View-vs-ViewNativeComponent-benchmark-itest.js @@ -0,0 +1,60 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import measureRenderTime from '../../../../src/private/__tests__/utilities/measureRenderTime'; +import ViewNativeComponent from '../ViewNativeComponent'; +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; +import {View} from 'react-native'; + +let root; +let testViews: React.MixedElement; + +const NUMBER_OF_VIEWS = 100; +const NUMBER_OF_ITERATIONS = 1000; + +component Noop(children: React.Node, style?: mixed) { + return children; +} + +Noop.displayName = 'Noop'; + +Fantom.unstable_benchmark + .suite('View vs. ViewNativeComponent', {minIterations: NUMBER_OF_ITERATIONS}) + .test.each( + [Noop, ViewNativeComponent, View], + Component => + `render ${NUMBER_OF_VIEWS} views (${Component.displayName ?? Component.name ?? 'ViewNativeComponent'})`, + () => { + return { + overriddenDuration: measureRenderTime(root, testViews), + }; + }, + Component => ({ + beforeAll: () => { + let views: React.Node = null; + for (let i = 0; i < NUMBER_OF_VIEWS; i++) { + views = ( + {views} + ); + } + // $FlowExpectedError[incompatible-type] + testViews = views; + }, + beforeEach: () => { + root = Fantom.createRoot(); + }, + afterEach: () => { + root.destroy(); + }, + }), + ); diff --git a/packages/react-native/src/private/__tests__/utilities/measureRenderTime.js b/packages/react-native/src/private/__tests__/utilities/measureRenderTime.js new file mode 100644 index 00000000000..938a85d3bf1 --- /dev/null +++ b/packages/react-native/src/private/__tests__/utilities/measureRenderTime.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import * as Fantom from '@react-native/fantom'; +import nullthrows from 'nullthrows'; + +const now = Fantom.unstable_benchmark.now; + +export default function measureRenderTime( + root: Fantom.Root, + elements: React.MixedElement, +): number { + let startTime: ?number; + let endTime: ?number; + + function RecordStart() { + startTime ??= now(); + return null; + } + + function RecordEnd() { + endTime = now(); + return null; + } + + Fantom.runTask(() => { + // React renders nodes using a depth-first algorithm. + root.render( + <> + + {elements} + + , + ); + }); + + return nullthrows(endTime) - nullthrows(startTime); +}