Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 61 additions & 22 deletions packages/react-native/Libraries/Components/View/View.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,66 @@ export type ViewInstance = HostInstance;
component View(ref?: React.RefSetter<ViewInstance>, ...props: ViewProps) {
const hasTextAncestor = use(TextAncestorContext);

// Fast path: most views use none of the props that need transforming, so
// skip copying and rewriting their props.
let resolvedProps = needsPropTransforms(props)
? transformProps(props)
: props;

if (__DEV__) {
const onSafeAreaInsetsChange =
resolvedProps.experimental_onSafeAreaInsetsChange;
if (onSafeAreaInsetsChange != null) {
// Copy rather than assign: on the fast path this is the incoming props.
resolvedProps = {
...resolvedProps,
experimental_onSafeAreaInsetsChange:
warnOnRepeatedSafeAreaInsetsChanges(onSafeAreaInsetsChange),
};
}
}

const actualView =
ref == null ? (
<ViewNativeComponent {...resolvedProps} />
) : (
<ViewNativeComponent {...resolvedProps} ref={ref} />
);

if (hasTextAncestor) {
return (
<TextAncestorContext value={false}>{actualView}</TextAncestorContext>
);
}
return actualView;
}

// Must check every prop that `transformProps` reads. Compares against
// `undefined` rather than `null` so that `null` values still go through
// `transformProps`, which strips them.
function needsPropTransforms(props: ViewProps): boolean {
return (
props.accessibilityState !== undefined ||
props.accessibilityValue !== undefined ||
props['aria-busy'] !== undefined ||
props['aria-checked'] !== undefined ||
props['aria-disabled'] !== undefined ||
props['aria-expanded'] !== undefined ||
props['aria-hidden'] !== undefined ||
props['aria-label'] !== undefined ||
props['aria-labelledby'] !== undefined ||
props['aria-live'] !== undefined ||
props['aria-selected'] !== undefined ||
props['aria-valuemax'] !== undefined ||
props['aria-valuemin'] !== undefined ||
props['aria-valuenow'] !== undefined ||
props['aria-valuetext'] !== undefined ||
props.id !== undefined ||
props.tabIndex !== undefined
);
}

function transformProps(props: ViewProps): ViewProps {
const {
accessibilityState,
accessibilityValue,
Expand Down Expand Up @@ -123,28 +183,7 @@ component View(ref?: React.RefSetter<ViewInstance>, ...props: ViewProps) {
};
}

if (__DEV__) {
const onSafeAreaInsetsChange =
resolvedProps.experimental_onSafeAreaInsetsChange;
if (onSafeAreaInsetsChange != null) {
resolvedProps.experimental_onSafeAreaInsetsChange =
warnOnRepeatedSafeAreaInsetsChanges(onSafeAreaInsetsChange);
}
}

const actualView =
ref == null ? (
<ViewNativeComponent {...resolvedProps} />
) : (
<ViewNativeComponent {...resolvedProps} ref={ref} />
);

if (hasTextAncestor) {
return (
<TextAncestorContext value={false}>{actualView}</TextAncestorContext>
);
}
return actualView;
return resolvedProps;
}

View.displayName = 'View';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* 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 * as Fantom from '@react-native/fantom';
import * as React from 'react';
import {View} from 'react-native';

const COUNT = 100;

let root: Fantom.Root;
let initial: React.MixedElement;
let next: React.MixedElement;

type Variant = Readonly<{
name: string,
update: boolean,
render: (i: number, gen: number) => React.Node,
}>;

const variants: ReadonlyArray<Variant> = [
{
name: 'mount, plain (fast path)',
update: false,
render: i => <View key={i} style={{width: i + 1, height: 1}} />,
},
{
name: 'update, plain (fast path)',
update: true,
render: (i, gen) => <View key={i} style={{width: i + gen, height: 1}} />,
},
{
name: 'mount, testID + pointerEvents (fast path)',
update: false,
render: i => (
<View
key={i}
testID={'v' + i}
pointerEvents="box-none"
style={{width: i + 1, height: 1}}
/>
),
},
{
name: 'mount, aria-label (slow path)',
update: false,
render: i => (
<View key={i} aria-label={'v' + i} style={{width: i + 1, height: 1}} />
),
},
{
name: 'update, aria-label (slow path)',
update: true,
render: (i, gen) => (
<View key={i} aria-label={'v' + i} style={{width: i + gen, height: 1}} />
),
},
{
name: 'mount, accessibilityState (slow path, like Pressable)',
update: false,
render: i => (
<View
key={i}
accessibilityState={{disabled: false}}
style={{width: i + 1, height: 1}}
/>
),
},
];

function build(variant: Variant, gen: number): React.MixedElement {
const children = [];
for (let i = 0; i < COUNT; i++) {
children.push(variant.render(i, gen));
}
return <>{children}</>;
}

Fantom.unstable_benchmark
.suite('View prop transforms', {minIterations: 1000})
.test.each(
variants,
variant => `${variant.name}, ${COUNT} views`,
() => ({overriddenDuration: measureRenderTime(root, next)}),
variant => ({
beforeAll: () => {
initial = build(variant, 1);
next = variant.update ? build(variant, 2) : initial;
},
beforeEach: () => {
root = Fantom.createRoot();
if (variant.update) {
// Same wrapper as the measured render, so React updates in place.
measureRenderTime(root, initial);
}
},
afterEach: () => {
root.destroy();
},
}),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* 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.
*
* @format
* @noflow
*/

'use strict';

const {parse} = require('flow-parser');
const fs = require('node:fs');
const path = require('node:path');

function getFunction(ast, name) {
const fn = ast.body.find(
node => node.type === 'FunctionDeclaration' && node.id.name === name,
);
if (fn == null) {
throw new Error(`Could not find \`${name}\` in View.js`);
}
return fn;
}

function keyName(node) {
return node.type === 'Identifier' ? node.name : String(node.value);
}

// Every `props.x` / `props['x']` read in `node`.
function collectPropReads(node, keys) {
if (node == null || typeof node !== 'object') {
return keys;
}
if (
node.type === 'MemberExpression' &&
node.object.type === 'Identifier' &&
node.object.name === 'props'
) {
keys.add(keyName(node.property));
}
for (const key of Object.keys(node)) {
if (key !== 'loc' && key !== 'range') {
const child = node[key];
if (Array.isArray(child)) {
child.forEach(c => collectPropReads(c, keys));
} else {
collectPropReads(child, keys);
}
}
}
return keys;
}

// The named keys `transformProps` destructures out of `props`.
function collectDestructuredKeys(fn) {
for (const statement of fn.body.body) {
if (statement.type !== 'VariableDeclaration') {
continue;
}
for (const declaration of statement.declarations) {
if (
declaration.id.type === 'ObjectPattern' &&
declaration.init?.type === 'Identifier' &&
declaration.init.name === 'props'
) {
return new Set(
declaration.id.properties
.filter(property => property.type === 'Property')
.map(property => keyName(property.key)),
);
}
}
}
throw new Error('Could not find the props destructure in `transformProps`');
}

describe('View prop transforms', () => {
it('skips the transform only when none of the transformed props are set', () => {
const source = fs.readFileSync(path.join(__dirname, '../View.js'), 'utf8');
const ast = parse(source, {components: true});

const checked = collectPropReads(
getFunction(ast, 'needsPropTransforms'),
new Set(),
);
const transformed = collectDestructuredKeys(
getFunction(ast, 'transformProps'),
);

// If this fails, a prop was added to (or removed from) `transformProps`
// without updating `needsPropTransforms`, so Views that only set that prop
// would skip the transform.
expect([...checked].sort()).toEqual([...transformed].sort());
expect(transformed.size).toBeGreaterThan(0);
});
});
Loading