Skip to content

Commit 5f960ef

Browse files
committed
Optimize VirtualizedList
1 parent 5f0bf8b commit 5f960ef

File tree

1 file changed

+148
-78
lines changed

1 file changed

+148
-78
lines changed

Libraries/Lists/VirtualizedList.js

Lines changed: 148 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,12 @@ type State = {
314314
last: number,
315315
};
316316

317+
type CachedCell = {
318+
component: any,
319+
deps: Array<any>,
320+
used?: boolean,
321+
};
322+
317323
/**
318324
* Default Props Helper Functions
319325
* Use the following helper functions for default values
@@ -784,8 +790,51 @@ class VirtualizedList extends React.PureComponent<Props, State> {
784790
};
785791
}
786792

793+
_cachedCells: Map<string, CachedCell> = new Map<string, CachedCell>();
794+
_cells = [];
795+
796+
_markCellsAsUnused() {
797+
this._cachedCells.forEach(cell => {
798+
cell.used = false;
799+
});
800+
}
801+
802+
_deleteUnusedCells() {
803+
this._cachedCells.forEach((cell, key) => {
804+
if (!cell.used) {
805+
this._cachedCells.delete(key);
806+
}
807+
});
808+
}
809+
810+
_pushCell(key, deps, creator) {
811+
//$FlowFixMe[incompatible-type]
812+
let cell: CachedCell = this._cachedCells.get(key);
813+
let depsChanged = false;
814+
if (!cell || cell.deps.length !== deps.length) {
815+
depsChanged = true;
816+
} else {
817+
for (let i = 0; i < deps.length; i++) {
818+
if (cell.deps[i] !== deps[i]) {
819+
depsChanged = true;
820+
break;
821+
}
822+
}
823+
}
824+
if (depsChanged) {
825+
this._cachedCells.set(
826+
key,
827+
(cell = {
828+
component: creator(),
829+
deps,
830+
}),
831+
);
832+
}
833+
cell.used = true;
834+
this._cells.push(cell.component);
835+
}
836+
787837
_pushCells(
788-
cells: Array<Object>,
789838
stickyHeaderIndices: Array<number>,
790839
stickyIndicesFromProps: Set<number>,
791840
first: number,
@@ -809,9 +858,19 @@ class VirtualizedList extends React.PureComponent<Props, State> {
809858
const key = this._keyExtractor(item, ii);
810859
this._indicesToKeys.set(ii, key);
811860
if (stickyIndicesFromProps.has(ii + stickyOffset)) {
812-
stickyHeaderIndices.push(cells.length);
861+
stickyHeaderIndices.push(this._cells.length);
813862
}
814-
cells.push(
863+
const deps = [
864+
CellRendererComponent,
865+
ItemSeparatorComponent,
866+
horizontal,
867+
ii,
868+
inversionStyle,
869+
item,
870+
prevCellKey,
871+
this.props,
872+
];
873+
this._pushCell(key, deps, () => (
815874
<CellRenderer
816875
CellRendererComponent={CellRendererComponent}
817876
ItemSeparatorComponent={ii < end ? ItemSeparatorComponent : undefined}
@@ -830,8 +889,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
830889
ref={ref => {
831890
this._cellRefs[key] = ref;
832891
}}
833-
/>,
834-
);
892+
/>
893+
));
835894
prevCellKey = key;
836895
}
837896
}
@@ -895,37 +954,46 @@ class VirtualizedList extends React.PureComponent<Props, State> {
895954
? styles.horizontallyInverted
896955
: styles.verticallyInverted
897956
: null;
898-
const cells = [];
957+
this._cells = [];
958+
this._markCellsAsUnused();
899959
const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices);
900960
const stickyHeaderIndices = [];
901961
if (ListHeaderComponent) {
902962
if (stickyIndicesFromProps.has(0)) {
903963
stickyHeaderIndices.push(0);
904964
}
905-
const element = React.isValidElement(ListHeaderComponent) ? (
906-
ListHeaderComponent
907-
) : (
908-
// $FlowFixMe[not-a-component]
909-
// $FlowFixMe[incompatible-type-arg]
910-
<ListHeaderComponent />
911-
);
912-
cells.push(
913-
<VirtualizedListCellContextProvider
914-
cellKey={this._getCellKey() + '-header'}
915-
key="$header">
916-
<View
917-
onLayout={this._onLayoutHeader}
918-
style={StyleSheet.compose(
919-
inversionStyle,
920-
this.props.ListHeaderComponentStyle,
921-
)}>
922-
{
923-
// $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors
924-
element
925-
}
926-
</View>
927-
</VirtualizedListCellContextProvider>,
928-
);
965+
const deps = [
966+
inversionStyle,
967+
ListHeaderComponent,
968+
this.props.extraData,
969+
this.props.ListHeaderComponentStyle,
970+
];
971+
this._pushCell('$header', deps, () => {
972+
const element = React.isValidElement(ListHeaderComponent) ? (
973+
ListHeaderComponent
974+
) : (
975+
// $FlowFixMe[not-a-component]
976+
// $FlowFixMe[incompatible-type-arg]
977+
<ListHeaderComponent />
978+
);
979+
return (
980+
<VirtualizedListCellContextProvider
981+
cellKey={this._getCellKey() + '-header'}
982+
key="$header">
983+
<View
984+
onLayout={this._onLayoutHeader}
985+
style={StyleSheet.compose(
986+
inversionStyle,
987+
this.props.ListHeaderComponentStyle,
988+
)}>
989+
{
990+
// $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors
991+
element
992+
}
993+
</View>
994+
</VirtualizedListCellContextProvider>
995+
);
996+
});
929997
}
930998
const itemCount = this.props.getItemCount(data);
931999
if (itemCount > 0) {
@@ -937,7 +1005,6 @@ class VirtualizedList extends React.PureComponent<Props, State> {
9371005
: initialNumToRenderOrDefault(this.props.initialNumToRender) - 1;
9381006
const {first, last} = this.state;
9391007
this._pushCells(
940-
cells,
9411008
stickyHeaderIndices,
9421009
stickyIndicesFromProps,
9431010
0,
@@ -958,11 +1025,10 @@ class VirtualizedList extends React.PureComponent<Props, State> {
9581025
stickyBlock.offset -
9591026
initBlock.offset -
9601027
(this.props.initialScrollIndex ? 0 : initBlock.length);
961-
cells.push(
1028+
this._cells.push(
9621029
<View key="$sticky_lead" style={{[spacerKey]: leadSpace}} />,
9631030
);
9641031
this._pushCells(
965-
cells,
9661032
stickyHeaderIndices,
9671033
stickyIndicesFromProps,
9681034
ii,
@@ -972,7 +1038,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
9721038
const trailSpace =
9731039
this._getFrameMetricsApprox(first).offset -
9741040
(stickyBlock.offset + stickyBlock.length);
975-
cells.push(
1041+
this._cells.push(
9761042
<View key="$sticky_trail" style={{[spacerKey]: trailSpace}} />,
9771043
);
9781044
insertedStickySpacer = true;
@@ -985,13 +1051,12 @@ class VirtualizedList extends React.PureComponent<Props, State> {
9851051
const firstSpace =
9861052
this._getFrameMetricsApprox(first).offset -
9871053
(initBlock.offset + initBlock.length);
988-
cells.push(
1054+
this._cells.push(
9891055
<View key="$lead_spacer" style={{[spacerKey]: firstSpace}} />,
9901056
);
9911057
}
9921058
}
9931059
this._pushCells(
994-
cells,
9951060
stickyHeaderIndices,
9961061
stickyIndicesFromProps,
9971062
firstAfterInitial,
@@ -1019,7 +1084,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
10191084
endFrame.offset +
10201085
endFrame.length -
10211086
(lastFrame.offset + lastFrame.length);
1022-
cells.push(
1087+
this._cells.push(
10231088
<View key="$tail_spacer" style={{[spacerKey]: tailSpacerLength}} />,
10241089
);
10251090
}
@@ -1033,7 +1098,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
10331098
// $FlowFixMe[incompatible-type-arg]
10341099
<ListEmptyComponent />
10351100
)): any);
1036-
cells.push(
1101+
1102+
this._cells.push(
10371103
React.cloneElement(element, {
10381104
key: '$empty',
10391105
onLayout: event => {
@@ -1047,30 +1113,38 @@ class VirtualizedList extends React.PureComponent<Props, State> {
10471113
);
10481114
}
10491115
if (ListFooterComponent) {
1050-
const element = React.isValidElement(ListFooterComponent) ? (
1051-
ListFooterComponent
1052-
) : (
1053-
// $FlowFixMe[not-a-component]
1054-
// $FlowFixMe[incompatible-type-arg]
1055-
<ListFooterComponent />
1056-
);
1057-
cells.push(
1058-
<VirtualizedListCellContextProvider
1059-
cellKey={this._getFooterCellKey()}
1060-
key="$footer">
1061-
<View
1062-
onLayout={this._onLayoutFooter}
1063-
style={StyleSheet.compose(
1064-
inversionStyle,
1065-
this.props.ListFooterComponentStyle,
1066-
)}>
1067-
{
1068-
// $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors
1069-
element
1070-
}
1071-
</View>
1072-
</VirtualizedListCellContextProvider>,
1073-
);
1116+
const deps = [
1117+
inversionStyle,
1118+
ListFooterComponent,
1119+
this.props.extraData,
1120+
this.props.ListFooterComponentStyle,
1121+
];
1122+
this._pushCell('$footer', deps, () => {
1123+
const element = React.isValidElement(ListFooterComponent) ? (
1124+
ListFooterComponent
1125+
) : (
1126+
// $FlowFixMe[not-a-component]
1127+
// $FlowFixMe[incompatible-type-arg]
1128+
<ListFooterComponent />
1129+
);
1130+
return (
1131+
<VirtualizedListCellContextProvider
1132+
cellKey={this._getFooterCellKey()}
1133+
key="$footer">
1134+
<View
1135+
onLayout={this._onLayoutFooter}
1136+
style={StyleSheet.compose(
1137+
inversionStyle,
1138+
this.props.ListFooterComponentStyle,
1139+
)}>
1140+
{
1141+
// $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors
1142+
element
1143+
}
1144+
</View>
1145+
</VirtualizedListCellContextProvider>
1146+
);
1147+
});
10741148
}
10751149
const scrollProps = {
10761150
...this.props,
@@ -1117,11 +1191,12 @@ class VirtualizedList extends React.PureComponent<Props, State> {
11171191
{
11181192
ref: this._captureScrollRef,
11191193
},
1120-
cells,
1194+
this._cells,
11211195
)}
11221196
</VirtualizedListContextProvider>
11231197
);
11241198
let ret = innerRet;
1199+
this._deleteUnusedCells();
11251200
if (__DEV__) {
11261201
ret = (
11271202
<ScrollView.Context.Consumer>
@@ -1931,33 +2006,27 @@ type CellRendererProps = {
19312006
};
19322007

19332008
type CellRendererState = {
1934-
separatorProps: $ReadOnly<{|
1935-
highlighted: boolean,
1936-
leadingItem: ?Item,
1937-
|}>,
2009+
highlighted: boolean,
2010+
leadingItem: ?Item,
19382011
...
19392012
};
19402013

1941-
class CellRenderer extends React.Component<
2014+
class CellRenderer extends React.PureComponent<
19422015
CellRendererProps,
19432016
CellRendererState,
19442017
> {
19452018
state = {
1946-
separatorProps: {
1947-
highlighted: false,
1948-
leadingItem: this.props.item,
1949-
},
2019+
highlighted: false,
2020+
leadingItem: this.props.item,
19502021
};
19512022

19522023
static getDerivedStateFromProps(
19532024
props: CellRendererProps,
19542025
prevState: CellRendererState,
19552026
): ?CellRendererState {
19562027
return {
1957-
separatorProps: {
1958-
...prevState.separatorProps,
1959-
leadingItem: props.item,
1960-
},
2028+
...prevState,
2029+
leadingItem: props.item,
19612030
};
19622031
}
19632032

@@ -1987,7 +2056,8 @@ class CellRenderer extends React.Component<
19872056

19882057
updateSeparatorProps(newProps: Object) {
19892058
this.setState(state => ({
1990-
separatorProps: {...state.separatorProps, ...newProps},
2059+
...state,
2060+
...newProps,
19912061
}));
19922062
}
19932063

@@ -2060,7 +2130,7 @@ class CellRenderer extends React.Component<
20602130
// NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and
20612131
// called explicitly by `ScrollViewStickyHeader`.
20622132
const itemSeparator = ItemSeparatorComponent && (
2063-
<ItemSeparatorComponent {...this.state.separatorProps} />
2133+
<ItemSeparatorComponent {...this.state} />
20642134
);
20652135
const cellStyle = inversionStyle
20662136
? horizontal

0 commit comments

Comments
 (0)