Skip to content

Commit 4f7fee2

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 83eb67a commit 4f7fee2

2 files changed

Lines changed: 169 additions & 22 deletions

File tree

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

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,64 @@ export type ViewInstance = HostInstance;
3939
component View(ref?: React.RefSetter<ViewInstance>, ...props: ViewProps) {
4040
const hasTextAncestor = use(TextAncestorContext);
4141

42+
// Fast path: most views use none of the props that need transforming, so
43+
// skip copying and rewriting their props.
44+
let resolvedProps = needsPropTransforms(props)
45+
? transformProps(props)
46+
: props;
47+
48+
if (__DEV__) {
49+
const onSafeAreaInsetsChange =
50+
resolvedProps.experimental_onSafeAreaInsetsChange;
51+
if (onSafeAreaInsetsChange != null) {
52+
// Copy rather than assign: on the fast path this is the incoming props.
53+
resolvedProps = {
54+
...resolvedProps,
55+
experimental_onSafeAreaInsetsChange:
56+
warnOnRepeatedSafeAreaInsetsChanges(onSafeAreaInsetsChange),
57+
};
58+
}
59+
}
60+
61+
const actualView =
62+
ref == null ? (
63+
<ViewNativeComponent {...resolvedProps} />
64+
) : (
65+
<ViewNativeComponent {...resolvedProps} ref={ref} />
66+
);
67+
68+
if (hasTextAncestor) {
69+
return (
70+
<TextAncestorContext value={false}>{actualView}</TextAncestorContext>
71+
);
72+
}
73+
return actualView;
74+
}
75+
76+
// Must check every prop that `transformProps` reads.
77+
function needsPropTransforms(props: ViewProps): boolean {
78+
return (
79+
props.accessibilityState !== undefined ||
80+
props.accessibilityValue !== undefined ||
81+
props['aria-busy'] !== undefined ||
82+
props['aria-checked'] !== undefined ||
83+
props['aria-disabled'] !== undefined ||
84+
props['aria-expanded'] !== undefined ||
85+
props['aria-hidden'] !== undefined ||
86+
props['aria-label'] !== undefined ||
87+
props['aria-labelledby'] !== undefined ||
88+
props['aria-live'] !== undefined ||
89+
props['aria-selected'] !== undefined ||
90+
props['aria-valuemax'] !== undefined ||
91+
props['aria-valuemin'] !== undefined ||
92+
props['aria-valuenow'] !== undefined ||
93+
props['aria-valuetext'] !== undefined ||
94+
props.id !== undefined ||
95+
props.tabIndex !== undefined
96+
);
97+
}
98+
99+
function transformProps(props: ViewProps): ViewProps {
42100
const {
43101
accessibilityState,
44102
accessibilityValue,
@@ -123,28 +181,7 @@ component View(ref?: React.RefSetter<ViewInstance>, ...props: ViewProps) {
123181
};
124182
}
125183

126-
if (__DEV__) {
127-
const onSafeAreaInsetsChange =
128-
resolvedProps.experimental_onSafeAreaInsetsChange;
129-
if (onSafeAreaInsetsChange != null) {
130-
resolvedProps.experimental_onSafeAreaInsetsChange =
131-
warnOnRepeatedSafeAreaInsetsChanges(onSafeAreaInsetsChange);
132-
}
133-
}
134-
135-
const actualView =
136-
ref == null ? (
137-
<ViewNativeComponent {...resolvedProps} />
138-
) : (
139-
<ViewNativeComponent {...resolvedProps} ref={ref} />
140-
);
141-
142-
if (hasTextAncestor) {
143-
return (
144-
<TextAncestorContext value={false}>{actualView}</TextAncestorContext>
145-
);
146-
}
147-
return actualView;
184+
return resolvedProps;
148185
}
149186

150187
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)