Skip to content

Commit c30e425

Browse files
shubh73claude
andcommitted
Skip View prop transforms when no transformed props are passed
View copies its props and checks 17 keys on every render to map aria-*, id and tabIndex onto their native equivalents. Most Views pass none of them, so check first and pass props through unchanged in that case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d7ff82e commit c30e425

2 files changed

Lines changed: 156 additions & 13 deletions

File tree

‎packages/react-native/Libraries/Components/View/View.js‎

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,51 @@ export type ViewInstance = HostInstance;
3131
component View(ref?: React.RefSetter<ViewInstance>, ...props: ViewProps) {
3232
const hasTextAncestor = use(TextAncestorContext);
3333

34+
// Fast path: most views use none of the props that need transforming, so
35+
// skip copying and rewriting their props.
36+
const resolvedProps = needsPropTransforms(props)
37+
? transformProps(props)
38+
: props;
39+
40+
const actualView =
41+
ref == null ? (
42+
<ViewNativeComponent {...resolvedProps} />
43+
) : (
44+
<ViewNativeComponent {...resolvedProps} ref={ref} />
45+
);
46+
47+
if (hasTextAncestor) {
48+
return (
49+
<TextAncestorContext value={false}>{actualView}</TextAncestorContext>
50+
);
51+
}
52+
return actualView;
53+
}
54+
55+
// Must check every prop that `transformProps` reads.
56+
function needsPropTransforms(props: ViewProps): boolean {
57+
return (
58+
props.accessibilityState !== undefined ||
59+
props.accessibilityValue !== undefined ||
60+
props['aria-busy'] !== undefined ||
61+
props['aria-checked'] !== undefined ||
62+
props['aria-disabled'] !== undefined ||
63+
props['aria-expanded'] !== undefined ||
64+
props['aria-hidden'] !== undefined ||
65+
props['aria-label'] !== undefined ||
66+
props['aria-labelledby'] !== undefined ||
67+
props['aria-live'] !== undefined ||
68+
props['aria-selected'] !== undefined ||
69+
props['aria-valuemax'] !== undefined ||
70+
props['aria-valuemin'] !== undefined ||
71+
props['aria-valuenow'] !== undefined ||
72+
props['aria-valuetext'] !== undefined ||
73+
props.id !== undefined ||
74+
props.tabIndex !== undefined
75+
);
76+
}
77+
78+
function transformProps(props: ViewProps): ViewProps {
3479
const {
3580
accessibilityState,
3681
accessibilityValue,
@@ -115,19 +160,7 @@ component View(ref?: React.RefSetter<ViewInstance>, ...props: ViewProps) {
115160
};
116161
}
117162

118-
const actualView =
119-
ref == null ? (
120-
<ViewNativeComponent {...resolvedProps} />
121-
) : (
122-
<ViewNativeComponent {...resolvedProps} ref={ref} />
123-
);
124-
125-
if (hasTextAncestor) {
126-
return (
127-
<TextAncestorContext value={false}>{actualView}</TextAncestorContext>
128-
);
129-
}
130-
return actualView;
163+
return resolvedProps;
131164
}
132165

133166
View.displayName = 'View';
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
12+
13+
import measureRenderTime from '../../../../src/private/__tests__/utilities/measureRenderTime';
14+
import * as Fantom from '@react-native/fantom';
15+
import * as React from 'react';
16+
import {View} from 'react-native';
17+
18+
const COUNT = 100;
19+
20+
let root: Fantom.Root;
21+
let initial: React.MixedElement;
22+
let next: React.MixedElement;
23+
24+
type Variant = Readonly<{
25+
name: string,
26+
update: boolean,
27+
render: (i: number, gen: number) => React.Node,
28+
}>;
29+
30+
const variants: ReadonlyArray<Variant> = [
31+
{
32+
name: 'mount, plain (fast path)',
33+
update: false,
34+
render: i => <View key={i} style={{width: i + 1, height: 1}} />,
35+
},
36+
{
37+
name: 'update, plain (fast path)',
38+
update: true,
39+
render: (i, gen) => <View key={i} style={{width: i + gen, height: 1}} />,
40+
},
41+
{
42+
name: 'mount, testID + pointerEvents (fast path)',
43+
update: false,
44+
render: i => (
45+
<View
46+
key={i}
47+
testID={'v' + i}
48+
pointerEvents="box-none"
49+
style={{width: i + 1, height: 1}}
50+
/>
51+
),
52+
},
53+
{
54+
name: 'mount, aria-label (slow path)',
55+
update: false,
56+
render: i => (
57+
<View key={i} aria-label={'v' + i} style={{width: i + 1, height: 1}} />
58+
),
59+
},
60+
{
61+
name: 'update, aria-label (slow path)',
62+
update: true,
63+
render: (i, gen) => (
64+
<View key={i} aria-label={'v' + i} style={{width: i + gen, height: 1}} />
65+
),
66+
},
67+
{
68+
name: 'mount, accessibilityState (slow path, like Pressable)',
69+
update: false,
70+
render: i => (
71+
<View
72+
key={i}
73+
accessibilityState={{disabled: false}}
74+
style={{width: i + 1, height: 1}}
75+
/>
76+
),
77+
},
78+
];
79+
80+
function build(variant: Variant, gen: number): React.MixedElement {
81+
const children = [];
82+
for (let i = 0; i < COUNT; i++) {
83+
children.push(variant.render(i, gen));
84+
}
85+
return <>{children}</>;
86+
}
87+
88+
Fantom.unstable_benchmark
89+
.suite('View prop transforms', {minIterations: 1000})
90+
.test.each(
91+
variants,
92+
variant => `${variant.name}, ${COUNT} views`,
93+
() => ({overriddenDuration: measureRenderTime(root, next)}),
94+
variant => ({
95+
beforeAll: () => {
96+
initial = build(variant, 1);
97+
next = variant.update ? build(variant, 2) : initial;
98+
},
99+
beforeEach: () => {
100+
root = Fantom.createRoot();
101+
if (variant.update) {
102+
// Same wrapper as the measured render, so React updates in place.
103+
measureRenderTime(root, initial);
104+
}
105+
},
106+
afterEach: () => {
107+
root.destroy();
108+
},
109+
}),
110+
);

0 commit comments

Comments
 (0)