VirtualizedList optimization - avoid lambda creation in CellRenderer onLayout prop

Summary:
Problem:
All CellRenderers rerender every time the containing VirtualizedList is rerendered. This is due to the following:
- Lambda is created for each CellRenderer's onLayout prop on every VirtualizedList render (fixed in this diff)
- CellRenderer's parentProps prop changes on every VirtualizedList render

Changelog:
[Internal] - VirtualizedList optimization - avoid lambda creation in CellRenderer onLayout prop

Reviewed By: javache

Differential Revision: D35061321

fbshipit-source-id: ab16bda8418b692f1edb4bce87e25c34f6252b56
This commit is contained in:
Genki Kondo 2022-03-24 08:28:01 -07:00 коммит произвёл Facebook GitHub Bot
Родитель e3c88eb946
Коммит 19cf70266e
5 изменённых файлов: 128 добавлений и 11 удалений

Просмотреть файл

@ -170,7 +170,9 @@ class ScrollViewStickyHeader extends React.Component<Props, State> {
this.props.onLayout(event); this.props.onLayout(event);
const child = React.Children.only(this.props.children); const child = React.Children.only(this.props.children);
if (child.props.onLayout) { if (child.props.onCellLayout) {
child.props.onCellLayout(event, child.props.cellKey, child.props.index);
} else if (child.props.onLayout) {
child.props.onLayout(event); child.props.onLayout(event);
} }
}; };

Просмотреть файл

@ -34,6 +34,7 @@ import type {
ViewToken, ViewToken,
ViewabilityConfigCallbackPair, ViewabilityConfigCallbackPair,
} from './ViewabilityHelper'; } from './ViewabilityHelper';
import type {LayoutEvent} from '../Types/CoreEventTypes';
import { import {
VirtualizedListCellContextProvider, VirtualizedListCellContextProvider,
VirtualizedListContext, VirtualizedListContext,
@ -824,8 +825,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
item={item} item={item}
key={key} key={key}
prevCellKey={prevCellKey} prevCellKey={prevCellKey}
onCellLayout={this._onCellLayout}
onUpdateSeparators={this._onUpdateSeparators} onUpdateSeparators={this._onUpdateSeparators}
onLayout={e => this._onCellLayout(e, key, ii)}
onUnmount={this._onCellUnmount} onUnmount={this._onCellUnmount}
parentProps={this.props} parentProps={this.props}
ref={ref => { ref={ref => {
@ -1268,7 +1269,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
} }
}; };
_onCellLayout(e, cellKey, index) { _onCellLayout = (e: LayoutEvent, cellKey: string, index: number): void => {
const layout = e.nativeEvent.layout; const layout = e.nativeEvent.layout;
const next = { const next = {
offset: this._selectOffset(layout), offset: this._selectOffset(layout),
@ -1301,7 +1302,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
this._computeBlankness(); this._computeBlankness();
this._updateViewableItems(this.props.data); this._updateViewableItems(this.props.data);
} };
_onCellUnmount = (cellKey: string) => { _onCellUnmount = (cellKey: string) => {
const curr = this._frames[cellKey]; const curr = this._frames[cellKey];
@ -1380,7 +1381,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
} }
} }
_onLayout = (e: Object) => { _onLayout = (e: LayoutEvent) => {
if (this._isNestedWithSameOrientation()) { if (this._isNestedWithSameOrientation()) {
// Need to adjust our scroll metrics to be relative to our containing // Need to adjust our scroll metrics to be relative to our containing
// VirtualizedList before we can make claims about list item viewability // VirtualizedList before we can make claims about list item viewability
@ -1395,7 +1396,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
this._maybeCallOnEndReached(); this._maybeCallOnEndReached();
}; };
_onLayoutEmpty = e => { _onLayoutEmpty = (e: LayoutEvent) => {
this.props.onLayout && this.props.onLayout(e); this.props.onLayout && this.props.onLayout(e);
}; };
@ -1403,12 +1404,12 @@ class VirtualizedList extends React.PureComponent<Props, State> {
return this._getCellKey() + '-footer'; return this._getCellKey() + '-footer';
} }
_onLayoutFooter = e => { _onLayoutFooter = (e: LayoutEvent) => {
this._triggerRemeasureForChildListsInCell(this._getFooterCellKey()); this._triggerRemeasureForChildListsInCell(this._getFooterCellKey());
this._footerLength = this._selectLength(e.nativeEvent.layout); this._footerLength = this._selectLength(e.nativeEvent.layout);
}; };
_onLayoutHeader = e => { _onLayoutHeader = (e: LayoutEvent) => {
this._headerLength = this._selectLength(e.nativeEvent.layout); this._headerLength = this._selectLength(e.nativeEvent.layout);
}; };
@ -1893,7 +1894,7 @@ type CellRendererProps = {
inversionStyle: ViewStyleProp, inversionStyle: ViewStyleProp,
item: Item, item: Item,
// This is extracted by ScrollViewStickyHeader // This is extracted by ScrollViewStickyHeader
onLayout: (event: Object) => void, onCellLayout: (event: Object, cellKey: string, index: number) => void,
onUnmount: (cellKey: string) => void, onUnmount: (cellKey: string) => void,
onUpdateSeparators: (cellKeys: Array<?string>, props: Object) => void, onUpdateSeparators: (cellKeys: Array<?string>, props: Object) => void,
parentProps: { parentProps: {
@ -1980,6 +1981,15 @@ class CellRenderer extends React.Component<
this.props.onUnmount(this.props.cellKey); this.props.onUnmount(this.props.cellKey);
} }
_onLayout = (nativeEvent: LayoutEvent): void => {
this.props.onCellLayout &&
this.props.onCellLayout(
nativeEvent,
this.props.cellKey,
this.props.index,
);
};
_renderElement(renderItem, ListItemComponent, item, index) { _renderElement(renderItem, ListItemComponent, item, index) {
if (renderItem && ListItemComponent) { if (renderItem && ListItemComponent) {
console.warn( console.warn(
@ -2039,9 +2049,10 @@ class CellRenderer extends React.Component<
/* $FlowFixMe[prop-missing] (>=0.68.0 site=react_native_fb) This comment /* $FlowFixMe[prop-missing] (>=0.68.0 site=react_native_fb) This comment
* suppresses an error found when Flow v0.68 was deployed. To see the * suppresses an error found when Flow v0.68 was deployed. To see the
* error delete this comment and run Flow. */ * error delete this comment and run Flow. */
getItemLayout && !parentProps.debug && !fillRateHelper.enabled() (getItemLayout && !parentProps.debug && !fillRateHelper.enabled()) ||
!this.props.onCellLayout
? undefined ? undefined
: this.props.onLayout; : this._onLayout;
// NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and // NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and
// called explicitly by `ScrollViewStickyHeader`. // called explicitly by `ScrollViewStickyHeader`.
const itemSeparator = ItemSeparatorComponent && ( const itemSeparator = ItemSeparatorComponent && (

Просмотреть файл

@ -1448,6 +1448,36 @@ it('renders windowSize derived region at bottom', () => {
expect(component).toMatchSnapshot(); expect(component).toMatchSnapshot();
}); });
it('calls _onCellLayout properly', () => {
const items = [{key: 'i1'}, {key: 'i2'}, {key: 'i3'}];
const mock = jest.fn();
const component = ReactTestRenderer.create(
<VirtualizedList
data={items}
renderItem={({item}) => <item value={item.key} />}
getItem={(data, index) => data[index]}
getItemCount={data => data.length}
/>,
);
const virtualList: VirtualizedList = component.getInstance();
virtualList._onCellLayout = mock;
component.update(
<VirtualizedList
data={[...items, {key: 'i4'}]}
renderItem={({item}) => <item value={item.key} />}
getItem={(data, index) => data[index]}
getItemCount={data => data.length}
/>,
);
const cell = virtualList._cellRefs.i4;
const event = {
nativeEvent: {layout: {x: 0, y: 0, width: 50, height: 50}},
};
cell._onLayout(event);
expect(mock).toHaveBeenCalledWith(event, 'i4', 3);
expect(mock).not.toHaveBeenCalledWith(event, 'i3', 2);
});
function generateItems(count) { function generateItems(count) {
return Array(count) return Array(count)
.fill() .fill()

Просмотреть файл

@ -0,0 +1,72 @@
/**
* 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 type {RenderItemProps} from 'react-native/Libraries/Lists/VirtualizedList';
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
import * as React from 'react';
import {FlatList, StyleSheet, Text, View} from 'react-native';
const DATA = [
'Sticky Pizza',
'Burger',
'Sticky Risotto',
'French Fries',
'Sticky Onion Rings',
'Fried Shrimps',
'Water',
'Coke',
'Beer',
'Cheesecake',
'Ice Cream',
];
const STICKY_HEADER_INDICES = [0, 2, 4];
const Item = ({item, separators}: RenderItemProps<string>) => {
return (
<View style={styles.item}>
<Text style={styles.title}>{item}</Text>
</View>
);
};
export function FlatList_stickyHeaders(): React.Node {
return (
<FlatList
data={DATA}
keyExtractor={(item, index) => item + index}
style={styles.list}
stickyHeaderIndices={STICKY_HEADER_INDICES}
renderItem={Item}
/>
);
}
const styles = StyleSheet.create({
item: {
backgroundColor: 'pink',
padding: 20,
marginVertical: 8,
},
list: {
flex: 1,
},
title: {
fontSize: 24,
},
});
export default ({
title: 'Sticky Headers',
name: 'stickyHeaders',
description: 'Test sticky headers on FlatList',
render: () => <FlatList_stickyHeaders />,
}: RNTesterModuleExample);

Просмотреть файл

@ -16,6 +16,7 @@ import InvertedExample from './FlatList-inverted';
import onViewableItemsChangedExample from './FlatList-onViewableItemsChanged'; import onViewableItemsChangedExample from './FlatList-onViewableItemsChanged';
import WithSeparatorsExample from './FlatList-withSeparators'; import WithSeparatorsExample from './FlatList-withSeparators';
import MultiColumnExample from './FlatList-multiColumn'; import MultiColumnExample from './FlatList-multiColumn';
import StickyHeadersExample from './FlatList-stickyHeaders';
export default ({ export default ({
framework: 'React', framework: 'React',
@ -32,5 +33,6 @@ export default ({
onViewableItemsChangedExample, onViewableItemsChangedExample,
WithSeparatorsExample, WithSeparatorsExample,
MultiColumnExample, MultiColumnExample,
StickyHeadersExample,
], ],
}: RNTesterModule); }: RNTesterModule);