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);
|
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);
|
||||||
|
|
Загрузка…
Ссылка в новой задаче