Skip to content

Commit

Permalink
Create performance comparison example in RNTester (#38673)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #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
  • Loading branch information
Xin Chen authored and facebook-github-bot committed Aug 2, 2023
1 parent 1b1097d commit 3fded52
Show file tree
Hide file tree
Showing 7 changed files with 423 additions and 0 deletions.
91 changes: 91 additions & 0 deletions packages/rn-tester/js/examples/Performance/ItemList.js
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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 />}
/>
);
},
},
];
Original file line number Diff line number Diff line change
@@ -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,
};
Original file line number Diff line number Diff line change
@@ -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,
};
Loading

0 comments on commit 3fded52

Please sign in to comment.