Skip to content

Commit 19cf702

Browse files
genkikondofacebook-github-bot
authored andcommitted
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
1 parent e3c88eb commit 19cf702

File tree

5 files changed

+128
-11
lines changed

5 files changed

+128
-11
lines changed

Libraries/Components/ScrollView/ScrollViewStickyHeader.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,9 @@ class ScrollViewStickyHeader extends React.Component<Props, State> {
170170

171171
this.props.onLayout(event);
172172
const child = React.Children.only(this.props.children);
173-
if (child.props.onLayout) {
173+
if (child.props.onCellLayout) {
174+
child.props.onCellLayout(event, child.props.cellKey, child.props.index);
175+
} else if (child.props.onLayout) {
174176
child.props.onLayout(event);
175177
}
176178
};

Libraries/Lists/VirtualizedList.js

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import type {
3434
ViewToken,
3535
ViewabilityConfigCallbackPair,
3636
} from './ViewabilityHelper';
37+
import type {LayoutEvent} from '../Types/CoreEventTypes';
3738
import {
3839
VirtualizedListCellContextProvider,
3940
VirtualizedListContext,
@@ -824,8 +825,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
824825
item={item}
825826
key={key}
826827
prevCellKey={prevCellKey}
828+
onCellLayout={this._onCellLayout}
827829
onUpdateSeparators={this._onUpdateSeparators}
828-
onLayout={e => this._onCellLayout(e, key, ii)}
829830
onUnmount={this._onCellUnmount}
830831
parentProps={this.props}
831832
ref={ref => {
@@ -1268,7 +1269,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
12681269
}
12691270
};
12701271

1271-
_onCellLayout(e, cellKey, index) {
1272+
_onCellLayout = (e: LayoutEvent, cellKey: string, index: number): void => {
12721273
const layout = e.nativeEvent.layout;
12731274
const next = {
12741275
offset: this._selectOffset(layout),
@@ -1301,7 +1302,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
13011302

13021303
this._computeBlankness();
13031304
this._updateViewableItems(this.props.data);
1304-
}
1305+
};
13051306

13061307
_onCellUnmount = (cellKey: string) => {
13071308
const curr = this._frames[cellKey];
@@ -1380,7 +1381,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
13801381
}
13811382
}
13821383

1383-
_onLayout = (e: Object) => {
1384+
_onLayout = (e: LayoutEvent) => {
13841385
if (this._isNestedWithSameOrientation()) {
13851386
// Need to adjust our scroll metrics to be relative to our containing
13861387
// VirtualizedList before we can make claims about list item viewability
@@ -1395,20 +1396,20 @@ class VirtualizedList extends React.PureComponent<Props, State> {
13951396
this._maybeCallOnEndReached();
13961397
};
13971398

1398-
_onLayoutEmpty = e => {
1399+
_onLayoutEmpty = (e: LayoutEvent) => {
13991400
this.props.onLayout && this.props.onLayout(e);
14001401
};
14011402

14021403
_getFooterCellKey(): string {
14031404
return this._getCellKey() + '-footer';
14041405
}
14051406

1406-
_onLayoutFooter = e => {
1407+
_onLayoutFooter = (e: LayoutEvent) => {
14071408
this._triggerRemeasureForChildListsInCell(this._getFooterCellKey());
14081409
this._footerLength = this._selectLength(e.nativeEvent.layout);
14091410
};
14101411

1411-
_onLayoutHeader = e => {
1412+
_onLayoutHeader = (e: LayoutEvent) => {
14121413
this._headerLength = this._selectLength(e.nativeEvent.layout);
14131414
};
14141415

@@ -1893,7 +1894,7 @@ type CellRendererProps = {
18931894
inversionStyle: ViewStyleProp,
18941895
item: Item,
18951896
// This is extracted by ScrollViewStickyHeader
1896-
onLayout: (event: Object) => void,
1897+
onCellLayout: (event: Object, cellKey: string, index: number) => void,
18971898
onUnmount: (cellKey: string) => void,
18981899
onUpdateSeparators: (cellKeys: Array<?string>, props: Object) => void,
18991900
parentProps: {
@@ -1980,6 +1981,15 @@ class CellRenderer extends React.Component<
19801981
this.props.onUnmount(this.props.cellKey);
19811982
}
19821983

1984+
_onLayout = (nativeEvent: LayoutEvent): void => {
1985+
this.props.onCellLayout &&
1986+
this.props.onCellLayout(
1987+
nativeEvent,
1988+
this.props.cellKey,
1989+
this.props.index,
1990+
);
1991+
};
1992+
19831993
_renderElement(renderItem, ListItemComponent, item, index) {
19841994
if (renderItem && ListItemComponent) {
19851995
console.warn(
@@ -2039,9 +2049,10 @@ class CellRenderer extends React.Component<
20392049
/* $FlowFixMe[prop-missing] (>=0.68.0 site=react_native_fb) This comment
20402050
* suppresses an error found when Flow v0.68 was deployed. To see the
20412051
* error delete this comment and run Flow. */
2042-
getItemLayout && !parentProps.debug && !fillRateHelper.enabled()
2052+
(getItemLayout && !parentProps.debug && !fillRateHelper.enabled()) ||
2053+
!this.props.onCellLayout
20432054
? undefined
2044-
: this.props.onLayout;
2055+
: this._onLayout;
20452056
// NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and
20462057
// called explicitly by `ScrollViewStickyHeader`.
20472058
const itemSeparator = ItemSeparatorComponent && (

Libraries/Lists/__tests__/VirtualizedList-test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1448,6 +1448,36 @@ it('renders windowSize derived region at bottom', () => {
14481448
expect(component).toMatchSnapshot();
14491449
});
14501450

1451+
it('calls _onCellLayout properly', () => {
1452+
const items = [{key: 'i1'}, {key: 'i2'}, {key: 'i3'}];
1453+
const mock = jest.fn();
1454+
const component = ReactTestRenderer.create(
1455+
<VirtualizedList
1456+
data={items}
1457+
renderItem={({item}) => <item value={item.key} />}
1458+
getItem={(data, index) => data[index]}
1459+
getItemCount={data => data.length}
1460+
/>,
1461+
);
1462+
const virtualList: VirtualizedList = component.getInstance();
1463+
virtualList._onCellLayout = mock;
1464+
component.update(
1465+
<VirtualizedList
1466+
data={[...items, {key: 'i4'}]}
1467+
renderItem={({item}) => <item value={item.key} />}
1468+
getItem={(data, index) => data[index]}
1469+
getItemCount={data => data.length}
1470+
/>,
1471+
);
1472+
const cell = virtualList._cellRefs.i4;
1473+
const event = {
1474+
nativeEvent: {layout: {x: 0, y: 0, width: 50, height: 50}},
1475+
};
1476+
cell._onLayout(event);
1477+
expect(mock).toHaveBeenCalledWith(event, 'i4', 3);
1478+
expect(mock).not.toHaveBeenCalledWith(event, 'i3', 2);
1479+
});
1480+
14511481
function generateItems(count) {
14521482
return Array(count)
14531483
.fill()
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import type {RenderItemProps} from 'react-native/Libraries/Lists/VirtualizedList';
12+
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
13+
14+
import * as React from 'react';
15+
import {FlatList, StyleSheet, Text, View} from 'react-native';
16+
17+
const DATA = [
18+
'Sticky Pizza',
19+
'Burger',
20+
'Sticky Risotto',
21+
'French Fries',
22+
'Sticky Onion Rings',
23+
'Fried Shrimps',
24+
'Water',
25+
'Coke',
26+
'Beer',
27+
'Cheesecake',
28+
'Ice Cream',
29+
];
30+
31+
const STICKY_HEADER_INDICES = [0, 2, 4];
32+
33+
const Item = ({item, separators}: RenderItemProps<string>) => {
34+
return (
35+
<View style={styles.item}>
36+
<Text style={styles.title}>{item}</Text>
37+
</View>
38+
);
39+
};
40+
41+
export function FlatList_stickyHeaders(): React.Node {
42+
return (
43+
<FlatList
44+
data={DATA}
45+
keyExtractor={(item, index) => item + index}
46+
style={styles.list}
47+
stickyHeaderIndices={STICKY_HEADER_INDICES}
48+
renderItem={Item}
49+
/>
50+
);
51+
}
52+
53+
const styles = StyleSheet.create({
54+
item: {
55+
backgroundColor: 'pink',
56+
padding: 20,
57+
marginVertical: 8,
58+
},
59+
list: {
60+
flex: 1,
61+
},
62+
title: {
63+
fontSize: 24,
64+
},
65+
});
66+
67+
export default ({
68+
title: 'Sticky Headers',
69+
name: 'stickyHeaders',
70+
description: 'Test sticky headers on FlatList',
71+
render: () => <FlatList_stickyHeaders />,
72+
}: RNTesterModuleExample);

packages/rn-tester/js/examples/FlatList/FlatListExampleIndex.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import InvertedExample from './FlatList-inverted';
1616
import onViewableItemsChangedExample from './FlatList-onViewableItemsChanged';
1717
import WithSeparatorsExample from './FlatList-withSeparators';
1818
import MultiColumnExample from './FlatList-multiColumn';
19+
import StickyHeadersExample from './FlatList-stickyHeaders';
1920

2021
export default ({
2122
framework: 'React',
@@ -32,5 +33,6 @@ export default ({
3233
onViewableItemsChangedExample,
3334
WithSeparatorsExample,
3435
MultiColumnExample,
36+
StickyHeadersExample,
3537
],
3638
}: RNTesterModule);

0 commit comments

Comments
 (0)