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:
Родитель
e3c88eb946
Коммит
19cf70266e
|
@ -170,7 +170,9 @@ class ScrollViewStickyHeader extends React.Component<Props, State> {
|
|||
|
||||
this.props.onLayout(event);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -34,6 +34,7 @@ import type {
|
|||
ViewToken,
|
||||
ViewabilityConfigCallbackPair,
|
||||
} from './ViewabilityHelper';
|
||||
import type {LayoutEvent} from '../Types/CoreEventTypes';
|
||||
import {
|
||||
VirtualizedListCellContextProvider,
|
||||
VirtualizedListContext,
|
||||
|
@ -824,8 +825,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
|
|||
item={item}
|
||||
key={key}
|
||||
prevCellKey={prevCellKey}
|
||||
onCellLayout={this._onCellLayout}
|
||||
onUpdateSeparators={this._onUpdateSeparators}
|
||||
onLayout={e => this._onCellLayout(e, key, ii)}
|
||||
onUnmount={this._onCellUnmount}
|
||||
parentProps={this.props}
|
||||
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 next = {
|
||||
offset: this._selectOffset(layout),
|
||||
|
@ -1301,7 +1302,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
|
|||
|
||||
this._computeBlankness();
|
||||
this._updateViewableItems(this.props.data);
|
||||
}
|
||||
};
|
||||
|
||||
_onCellUnmount = (cellKey: string) => {
|
||||
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()) {
|
||||
// Need to adjust our scroll metrics to be relative to our containing
|
||||
// VirtualizedList before we can make claims about list item viewability
|
||||
|
@ -1395,7 +1396,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
|
|||
this._maybeCallOnEndReached();
|
||||
};
|
||||
|
||||
_onLayoutEmpty = e => {
|
||||
_onLayoutEmpty = (e: LayoutEvent) => {
|
||||
this.props.onLayout && this.props.onLayout(e);
|
||||
};
|
||||
|
||||
|
@ -1403,12 +1404,12 @@ class VirtualizedList extends React.PureComponent<Props, State> {
|
|||
return this._getCellKey() + '-footer';
|
||||
}
|
||||
|
||||
_onLayoutFooter = e => {
|
||||
_onLayoutFooter = (e: LayoutEvent) => {
|
||||
this._triggerRemeasureForChildListsInCell(this._getFooterCellKey());
|
||||
this._footerLength = this._selectLength(e.nativeEvent.layout);
|
||||
};
|
||||
|
||||
_onLayoutHeader = e => {
|
||||
_onLayoutHeader = (e: LayoutEvent) => {
|
||||
this._headerLength = this._selectLength(e.nativeEvent.layout);
|
||||
};
|
||||
|
||||
|
@ -1893,7 +1894,7 @@ type CellRendererProps = {
|
|||
inversionStyle: ViewStyleProp,
|
||||
item: Item,
|
||||
// This is extracted by ScrollViewStickyHeader
|
||||
onLayout: (event: Object) => void,
|
||||
onCellLayout: (event: Object, cellKey: string, index: number) => void,
|
||||
onUnmount: (cellKey: string) => void,
|
||||
onUpdateSeparators: (cellKeys: Array<?string>, props: Object) => void,
|
||||
parentProps: {
|
||||
|
@ -1980,6 +1981,15 @@ class CellRenderer extends React.Component<
|
|||
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) {
|
||||
if (renderItem && ListItemComponent) {
|
||||
console.warn(
|
||||
|
@ -2039,9 +2049,10 @@ class CellRenderer extends React.Component<
|
|||
/* $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
|
||||
* error delete this comment and run Flow. */
|
||||
getItemLayout && !parentProps.debug && !fillRateHelper.enabled()
|
||||
(getItemLayout && !parentProps.debug && !fillRateHelper.enabled()) ||
|
||||
!this.props.onCellLayout
|
||||
? undefined
|
||||
: this.props.onLayout;
|
||||
: this._onLayout;
|
||||
// NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and
|
||||
// called explicitly by `ScrollViewStickyHeader`.
|
||||
const itemSeparator = ItemSeparatorComponent && (
|
||||
|
|
|
@ -1448,6 +1448,36 @@ it('renders windowSize derived region at bottom', () => {
|
|||
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) {
|
||||
return Array(count)
|
||||
.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 WithSeparatorsExample from './FlatList-withSeparators';
|
||||
import MultiColumnExample from './FlatList-multiColumn';
|
||||
import StickyHeadersExample from './FlatList-stickyHeaders';
|
||||
|
||||
export default ({
|
||||
framework: 'React',
|
||||
|
@ -32,5 +33,6 @@ export default ({
|
|||
onViewableItemsChangedExample,
|
||||
WithSeparatorsExample,
|
||||
MultiColumnExample,
|
||||
StickyHeadersExample,
|
||||
],
|
||||
}: RNTesterModule);
|
||||
|
|
Загрузка…
Ссылка в новой задаче