diff --git a/packages/rn-tester/js/examples/Performance/ItemList.js b/packages/rn-tester/js/examples/Performance/ItemList.js new file mode 100644 index 00000000000000..622b013906d572 --- /dev/null +++ b/packages/rn-tester/js/examples/Performance/ItemList.js @@ -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 ( + + {data.name} + {`Age: ${data.age}`} + {`Address: ${data.address}`} + {`id: ${data.id}`} + + ); +} + +interface ItemListProps { + data: ItemDataType[]; + useFlatList?: boolean; + onScroll?: (evt: ScrollEvent) => void; +} + +function renderItem({item}: {item: ItemDataType, ...}): React.MixedElement { + return ; +} + +function ItemList(props: ItemListProps): React.Node { + const {data, useFlatList = false, onScroll} = props; + + return ( + + {useFlatList ? ( + + ) : ( + + {data.map(item => ( + + ))} + + )} + + ); +} + +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; diff --git a/packages/rn-tester/js/examples/Performance/PerformanceComparisonExample.js b/packages/rn-tester/js/examples/Performance/PerformanceComparisonExample.js new file mode 100644 index 00000000000000..6b71ccf6371efc --- /dev/null +++ b/packages/rn-tester/js/examples/Performance/PerformanceComparisonExample.js @@ -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({badExampleScript}); + } + result.push({badExample}); + } + return result; + }, [loadExample, badExample, badExampleScript]); + + const goodExampleContents = useMemo(() => { + const result: React.Node[] = []; + if (loadExample === SHOW_GOOD_EXAMPLE) { + if (goodExampleScript != null) { + result.push({goodExampleScript}); + } + result.push({goodExample}); + } + return result; + }, [loadExample, goodExample, goodExampleScript]); + + return ( + + + + + {loadExample === SHOW_BAD_EXAMPLE ? 'Hide Bad' : 'Show Bad'} + + + {loadExample === SHOW_GOOD_EXAMPLE ? 'Hide Good' : 'Show Good'} + + + + {loadExample === SHOW_BAD_EXAMPLE + ? badExampleContents + : loadExample === SHOW_GOOD_EXAMPLE + ? goodExampleContents + : null} + + + + ); +} + +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 ( + } + goodExample={} + /> + ); + }, + }, + { + title: ReRenderWithObjectPropExample.title, + description: ReRenderWithObjectPropExample.description, + render: function (): React.Node { + return ( + } + goodExample={} + /> + ); + }, + }, +]; diff --git a/packages/rn-tester/js/examples/Performance/ReRenderWithNonPureChildExample.js b/packages/rn-tester/js/examples/Performance/ReRenderWithNonPureChildExample.js new file mode 100644 index 00000000000000..7c3c118233274a --- /dev/null +++ b/packages/rn-tester/js/examples/Performance/ReRenderWithNonPureChildExample.js @@ -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 ( + <> + {`Scroll Offset X: ${scrollOffset}`} + + + ); +} + +function ReRenderWithNonPureChildGoodExample(): React.Node { + const [scrollOffset, setScrollOffset] = useState(0); + const onScroll = useCallback( + (evt: ScrollEvent) => { + setScrollOffset(evt.nativeEvent.contentOffset.x); + }, + [setScrollOffset], + ); + + return ( + <> + {`Scroll Offset X: ${scrollOffset}`} + + + ); +} + +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, +}; diff --git a/packages/rn-tester/js/examples/Performance/ReRenderWithObjectPropExample.js b/packages/rn-tester/js/examples/Performance/ReRenderWithObjectPropExample.js new file mode 100644 index 00000000000000..e2991e724bc863 --- /dev/null +++ b/packages/rn-tester/js/examples/Performance/ReRenderWithObjectPropExample.js @@ -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 ( + <> + {`Scroll Offset X: ${scrollOffset}`} + { + 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 ( + <> + {`Scroll Offset X: ${scrollOffset}`} + + + ); +} + +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, +}; diff --git a/packages/rn-tester/js/examples/Performance/itemData.js b/packages/rn-tester/js/examples/Performance/itemData.js new file mode 100644 index 00000000000000..f5e60263ae6685 --- /dev/null +++ b/packages/rn-tester/js/examples/Performance/itemData.js @@ -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); diff --git a/packages/rn-tester/js/utils/RNTesterList.android.js b/packages/rn-tester/js/utils/RNTesterList.android.js index 429b19428a9b79..d702ef779ede31 100644 --- a/packages/rn-tester/js/utils/RNTesterList.android.js +++ b/packages/rn-tester/js/utils/RNTesterList.android.js @@ -130,6 +130,11 @@ const Components: Array = [ category: 'UI', module: require('../examples/NewArchitecture/NewArchitectureExample'), }, + { + key: 'PerformanceComparisonExample', + category: 'Basic', + module: require('../examples/Performance/PerformanceComparisonExample'), + }, ]; const APIs: Array = ([ diff --git a/packages/rn-tester/js/utils/RNTesterList.ios.js b/packages/rn-tester/js/utils/RNTesterList.ios.js index 88c793f68a2ee1..5287ec00761782 100644 --- a/packages/rn-tester/js/utils/RNTesterList.ios.js +++ b/packages/rn-tester/js/utils/RNTesterList.ios.js @@ -136,6 +136,11 @@ const Components: Array = [ category: 'UI', module: require('../examples/NewArchitecture/NewArchitectureExample'), }, + { + key: 'PerformanceComparisonExample', + category: 'Basic', + module: require('../examples/Performance/PerformanceComparisonExample'), + }, ]; const APIs: Array = ([