Create performance comparison example in RNTester (#38673)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/38673

This diff adds performance comparison examples to RNTester. In each of the comparison we have bad and good examples, which could be used for the following purposes:

- Collect common performance pitfalls
- Use as testbed on performance tools and metrics for validation hypothesis

Changelog:
[Internal] - Add performance comparison example in RNTester

Reviewed By: rshest

Differential Revision: D47821109

fbshipit-source-id: ea0242ea50724d27c7713bb116335a465e24d1a7
This commit is contained in:
Xin Chen
2023-08-01 19:08:19 -07:00
committed by Facebook GitHub Bot
parent 1b1097de1d
commit 3fded52fb4
7 changed files with 423 additions and 0 deletions
@@ -0,0 +1,91 @@
/**
* 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
*/
'use strict';
import type {ScrollEvent} from 'react-native/Libraries/Types/CoreEventTypes';
import type {ItemDataType} from './itemData';
import * as React from 'react';
import {StyleSheet, View, Text, FlatList, ScrollView} from 'react-native';
function Item(props: {data: ItemDataType}): React.Node {
const {data} = props;
return (
<View style={styles.itemContainer}>
<Text style={styles.itemName}>{data.name}</Text>
<Text>{`Age: ${data.age}`}</Text>
<Text>{`Address: ${data.address}`}</Text>
<Text>{`id: ${data.id}`}</Text>
</View>
);
}
interface ItemListProps {
data: ItemDataType[];
useFlatList?: boolean;
onScroll?: (evt: ScrollEvent) => void;
}
function renderItem({item}: {item: ItemDataType, ...}): React.MixedElement {
return <Item data={item} />;
}
function ItemList(props: ItemListProps): React.Node {
const {data, useFlatList = false, onScroll} = props;
return (
<View style={styles.container}>
{useFlatList ? (
<FlatList
horizontal
onScroll={onScroll}
data={data}
renderItem={renderItem}
/>
) : (
<ScrollView horizontal onScroll={onScroll} scrollEventThrottle={16}>
{data.map(item => (
<Item data={item} key={item.id} />
))}
</ScrollView>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'flex-start',
padding: 5,
},
itemContainer: {
width: 200,
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'flex-start',
padding: 5,
backgroundColor: 'gray',
marginHorizontal: 5,
},
itemName: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 5,
},
itemDescription: {
fontSize: 15,
},
});
export default ItemList;
@@ -0,0 +1,142 @@
/**
* 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
* @flow strict-local
* @oncall react_native
*/
'use strict';
import * as React from 'react';
import {StyleSheet, View, Text} from 'react-native';
import RNTesterPage from '../../components/RNTesterPage';
import RNTesterButton from '../../components/RNTesterButton';
import ReRenderWithObjectPropExample from './ReRenderWithObjectPropExample';
import ReRenderWithNonPureChildExample from './ReRenderWithNonPureChildExample';
const {useState, useCallback, useMemo} = React;
const SHOW_NOTHING = 'SHOW_NOTHING';
const SHOW_GOOD_EXAMPLE = 'SHOW_GOOD_EXAMPLE';
const SHOW_BAD_EXAMPLE = 'SHOW_BAD_EXAMPLE';
function PerfExampleWrapper(props: {
badExample: React.Node,
goodExample: React.Node,
badExampleScript?: string,
goodExampleScript?: string,
}): React.Node {
const {badExample, goodExample, badExampleScript, goodExampleScript} = props;
const [loadExample, setLoadExample] = useState(SHOW_NOTHING);
const toggleGoodExample = useCallback(
() =>
setLoadExample(
loadExample === SHOW_GOOD_EXAMPLE ? SHOW_NOTHING : SHOW_GOOD_EXAMPLE,
),
[setLoadExample, loadExample],
);
const toggleBadExample = useCallback(
() =>
setLoadExample(
loadExample === SHOW_BAD_EXAMPLE ? SHOW_NOTHING : SHOW_BAD_EXAMPLE,
),
[setLoadExample, loadExample],
);
const badExampleContents = useMemo(() => {
const result: React.Node[] = [];
if (loadExample === SHOW_BAD_EXAMPLE) {
if (badExampleScript != null) {
result.push(<Text key="1">{badExampleScript}</Text>);
}
result.push(<View key="2">{badExample}</View>);
}
return result;
}, [loadExample, badExample, badExampleScript]);
const goodExampleContents = useMemo(() => {
const result: React.Node[] = [];
if (loadExample === SHOW_GOOD_EXAMPLE) {
if (goodExampleScript != null) {
result.push(<Text key="1">{goodExampleScript}</Text>);
}
result.push(<View key="2">{goodExample}</View>);
}
return result;
}, [loadExample, goodExample, goodExampleScript]);
return (
<RNTesterPage noScroll={true}>
<View style={styles.container}>
<View style={styles.controls}>
<RNTesterButton onPress={toggleBadExample}>
{loadExample === SHOW_BAD_EXAMPLE ? 'Hide Bad' : 'Show Bad'}
</RNTesterButton>
<RNTesterButton onPress={toggleGoodExample}>
{loadExample === SHOW_GOOD_EXAMPLE ? 'Hide Good' : 'Show Good'}
</RNTesterButton>
</View>
<View style={styles.exampleContainer}>
{loadExample === SHOW_BAD_EXAMPLE
? badExampleContents
: loadExample === SHOW_GOOD_EXAMPLE
? goodExampleContents
: null}
</View>
</View>
</RNTesterPage>
);
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 5,
},
controls: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderBottomWidth: 1,
borderColor: 'gray',
marginBottom: 5,
},
exampleContainer: {
flex: 1,
backgroundColor: 'white',
},
perfExampleContainer: {},
});
exports.title = 'Performance Comparison Examples';
exports.category = 'Basic';
exports.description =
'Compare performance with bad and good examples. Use React DevTools to highlight re-renders is recommended.';
exports.examples = [
{
title: ReRenderWithNonPureChildExample.title,
description: ReRenderWithNonPureChildExample.description,
render: function (): React.Node {
return (
<PerfExampleWrapper
badExample={<ReRenderWithNonPureChildExample.Bad />}
goodExample={<ReRenderWithNonPureChildExample.Good />}
/>
);
},
},
{
title: ReRenderWithObjectPropExample.title,
description: ReRenderWithObjectPropExample.description,
render: function (): React.Node {
return (
<PerfExampleWrapper
badExample={<ReRenderWithObjectPropExample.Bad />}
goodExample={<ReRenderWithObjectPropExample.Good />}
/>
);
},
},
];
@@ -0,0 +1,63 @@
/**
* 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
*/
'use strict';
import * as React from 'react';
import {Text} from 'react-native';
import ItemList from './ItemList';
import {LIST_100_ITEMS} from './itemData';
import type {ScrollEvent} from 'react-native/Libraries/Types/CoreEventTypes';
const {useCallback, useState} = React;
const ItemListMemo = React.memo(ItemList);
function ReRenderWithNonPureChildBadExample(): React.Node {
const [scrollOffset, setScrollOffset] = useState(0);
const onScroll = useCallback(
(evt: ScrollEvent) => {
setScrollOffset(evt.nativeEvent.contentOffset.x);
},
[setScrollOffset],
);
return (
<>
<Text>{`Scroll Offset X: ${scrollOffset}`}</Text>
<ItemList data={LIST_100_ITEMS} onScroll={onScroll} />
</>
);
}
function ReRenderWithNonPureChildGoodExample(): React.Node {
const [scrollOffset, setScrollOffset] = useState(0);
const onScroll = useCallback(
(evt: ScrollEvent) => {
setScrollOffset(evt.nativeEvent.contentOffset.x);
},
[setScrollOffset],
);
return (
<>
<Text>{`Scroll Offset X: ${scrollOffset}`}</Text>
<ItemListMemo data={LIST_100_ITEMS} onScroll={onScroll} />
</>
);
}
export default {
title: 'List re-render due to not pure or memoized',
description:
'The List component is not pure in the bad example. Even though all props are not changed, it will still re-render when parent re-renders.',
Bad: ReRenderWithNonPureChildBadExample,
Good: ReRenderWithNonPureChildGoodExample,
};
@@ -0,0 +1,61 @@
/**
* 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
*/
'use strict';
import * as React from 'react';
import {Text} from 'react-native';
import ItemList from './ItemList';
import {LIST_100_ITEMS} from './itemData';
import type {ScrollEvent} from 'react-native/Libraries/Types/CoreEventTypes';
const {useState, useCallback} = React;
const ItemListMemo = React.memo(ItemList);
function ReRenderWithObjectPropBadExample(): React.Node {
const [scrollOffset, setScrollOffset] = useState(0);
return (
<>
<Text>{`Scroll Offset X: ${scrollOffset}`}</Text>
<ItemListMemo
data={LIST_100_ITEMS}
onScroll={(evt: ScrollEvent) => {
setScrollOffset(evt.nativeEvent.contentOffset.x);
}}
/>
</>
);
}
function ReRenderWithObjectPropGoodExample(): React.Node {
const [scrollOffset, setScrollOffset] = useState(0);
const onScroll = useCallback(
(evt: ScrollEvent) => {
setScrollOffset(evt.nativeEvent.contentOffset.x);
},
[setScrollOffset],
);
return (
<>
<Text>{`Scroll Offset X: ${scrollOffset}`}</Text>
<ItemListMemo data={LIST_100_ITEMS} onScroll={onScroll} />
</>
);
}
export default {
title: 'Re-render from new object reference in prop',
description:
'Even with pure or memoized child component, if a new object reference is passed down as prop, the child component will still re-render unnecessarily. The onScroll callback is passed without useCallback hook in the bad example and caused performance issues.',
Bad: ReRenderWithObjectPropBadExample,
Good: ReRenderWithObjectPropGoodExample,
};
@@ -0,0 +1,56 @@
/**
* 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
*/
const ALL_CHARS =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
function generateRandomString(length: number = 16): string {
let str = '';
for (let i = 0; i < length; i++) {
str += ALL_CHARS.charAt(Math.floor(Math.random() * ALL_CHARS.length));
}
return str;
}
function generateRandomAge(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function generateRandomName(): string {
return 'Joe ' + generateRandomString();
}
function generateRandomAddress(): string {
const city = generateRandomName() + ' City';
const state = generateRandomName() + ' State';
const country = generateRandomName() + ' Country';
return `${city}, ${state}, ${country}`;
}
export interface ItemDataType {
id: string;
name: string;
address: string;
age: number;
}
export function generateRandomItems(count: number): ItemDataType[] {
return Array.from(Array(count), () => ({
id: generateRandomString(),
name: generateRandomName(),
address: generateRandomAddress(),
age: generateRandomAge(13, 40),
}));
}
export const LIST_10_ITEMS: ItemDataType[] = generateRandomItems(10);
export const LIST_100_ITEMS: ItemDataType[] = generateRandomItems(100);
export const LIST_1000_ITEMS: ItemDataType[] = generateRandomItems(1000);
@@ -130,6 +130,11 @@ const Components: Array<RNTesterModuleInfo> = [
category: 'UI',
module: require('../examples/NewArchitecture/NewArchitectureExample'),
},
{
key: 'PerformanceComparisonExample',
category: 'Basic',
module: require('../examples/Performance/PerformanceComparisonExample'),
},
];
const APIs: Array<RNTesterModuleInfo> = ([
@@ -136,6 +136,11 @@ const Components: Array<RNTesterModuleInfo> = [
category: 'UI',
module: require('../examples/NewArchitecture/NewArchitectureExample'),
},
{
key: 'PerformanceComparisonExample',
category: 'Basic',
module: require('../examples/Performance/PerformanceComparisonExample'),
},
];
const APIs: Array<RNTesterModuleInfo> = ([