mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
1b1097de1d
commit
3fded52fb4
@@ -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> = ([
|
||||
|
||||
Reference in New Issue
Block a user