Skip to content

Commit cdf3dd6

Browse files
committed
Optimize VirtualizedList
1 parent 5f0bf8b commit cdf3dd6

File tree

1 file changed

+147
-90
lines changed

1 file changed

+147
-90
lines changed

Libraries/Lists/VirtualizedList.js

Lines changed: 147 additions & 90 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,40 @@ 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+
this._pushCell('$header', [], () => {
966+
const element = React.isValidElement(ListHeaderComponent) ? (
967+
ListHeaderComponent
968+
) : (
969+
// $FlowFixMe[not-a-component]
970+
// $FlowFixMe[incompatible-type-arg]
971+
<ListHeaderComponent />
972+
);
973+
return (
974+
<VirtualizedListCellContextProvider
975+
cellKey={this._getCellKey() + '-header'}
976+
key="$header">
977+
<View
978+
onLayout={this._onLayoutHeader}
979+
style={StyleSheet.compose(
980+
inversionStyle,
981+
this.props.ListHeaderComponentStyle,
982+
)}>
983+
{
984+
// $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors
985+
element
986+
}
987+
</View>
988+
</VirtualizedListCellContextProvider>
989+
);
990+
});
929991
}
930992
const itemCount = this.props.getItemCount(data);
931993
if (itemCount > 0) {
@@ -937,7 +999,6 @@ class VirtualizedList extends React.PureComponent<Props, State> {
937999
: initialNumToRenderOrDefault(this.props.initialNumToRender) - 1;
9381000
const {first, last} = this.state;
9391001
this._pushCells(
940-
cells,
9411002
stickyHeaderIndices,
9421003
stickyIndicesFromProps,
9431004
0,
@@ -958,11 +1019,10 @@ class VirtualizedList extends React.PureComponent<Props, State> {
9581019
stickyBlock.offset -
9591020
initBlock.offset -
9601021
(this.props.initialScrollIndex ? 0 : initBlock.length);
961-
cells.push(
1022+
this._cells.push(
9621023
<View key="$sticky_lead" style={{[spacerKey]: leadSpace}} />,
9631024
);
9641025
this._pushCells(
965-
cells,
9661026
stickyHeaderIndices,
9671027
stickyIndicesFromProps,
9681028
ii,
@@ -972,7 +1032,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
9721032
const trailSpace =
9731033
this._getFrameMetricsApprox(first).offset -
9741034
(stickyBlock.offset + stickyBlock.length);
975-
cells.push(
1035+
this._cells.push(
9761036
<View key="$sticky_trail" style={{[spacerKey]: trailSpace}} />,
9771037
);
9781038
insertedStickySpacer = true;
@@ -985,13 +1045,12 @@ class VirtualizedList extends React.PureComponent<Props, State> {
9851045
const firstSpace =
9861046
this._getFrameMetricsApprox(first).offset -
9871047
(initBlock.offset + initBlock.length);
988-
cells.push(
1048+
this._cells.push(
9891049
<View key="$lead_spacer" style={{[spacerKey]: firstSpace}} />,
9901050
);
9911051
}
9921052
}
9931053
this._pushCells(
994-
cells,
9951054
stickyHeaderIndices,
9961055
stickyIndicesFromProps,
9971056
firstAfterInitial,
@@ -1019,22 +1078,22 @@ class VirtualizedList extends React.PureComponent<Props, State> {
10191078
endFrame.offset +
10201079
endFrame.length -
10211080
(lastFrame.offset + lastFrame.length);
1022-
cells.push(
1081+
this._cells.push(
10231082
<View key="$tail_spacer" style={{[spacerKey]: tailSpacerLength}} />,
10241083
);
10251084
}
10261085
} else if (ListEmptyComponent) {
1027-
const element: React.Element<any> = ((React.isValidElement(
1028-
ListEmptyComponent,
1029-
) ? (
1030-
ListEmptyComponent
1031-
) : (
1032-
// $FlowFixMe[not-a-component]
1033-
// $FlowFixMe[incompatible-type-arg]
1034-
<ListEmptyComponent />
1035-
)): any);
1036-
cells.push(
1037-
React.cloneElement(element, {
1086+
this._pushCell('$empty', [], () => {
1087+
const element: React.Element<any> = ((React.isValidElement(
1088+
ListEmptyComponent,
1089+
) ? (
1090+
ListEmptyComponent
1091+
) : (
1092+
// $FlowFixMe[not-a-component]
1093+
// $FlowFixMe[incompatible-type-arg]
1094+
<ListEmptyComponent />
1095+
)): any);
1096+
return React.cloneElement(element, {
10381097
key: '$empty',
10391098
onLayout: event => {
10401099
this._onLayoutEmpty(event);
@@ -1043,34 +1102,36 @@ class VirtualizedList extends React.PureComponent<Props, State> {
10431102
}
10441103
},
10451104
style: StyleSheet.compose(inversionStyle, element.props.style),
1046-
}),
1047-
);
1105+
});
1106+
});
10481107
}
10491108
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-
);
1109+
this._pushCell('$footer', [], () => {
1110+
const element = React.isValidElement(ListFooterComponent) ? (
1111+
ListFooterComponent
1112+
) : (
1113+
// $FlowFixMe[not-a-component]
1114+
// $FlowFixMe[incompatible-type-arg]
1115+
<ListFooterComponent />
1116+
);
1117+
return (
1118+
<VirtualizedListCellContextProvider
1119+
cellKey={this._getFooterCellKey()}
1120+
key="$footer">
1121+
<View
1122+
onLayout={this._onLayoutFooter}
1123+
style={StyleSheet.compose(
1124+
inversionStyle,
1125+
this.props.ListFooterComponentStyle,
1126+
)}>
1127+
{
1128+
// $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors
1129+
element
1130+
}
1131+
</View>
1132+
</VirtualizedListCellContextProvider>
1133+
);
1134+
});
10741135
}
10751136
const scrollProps = {
10761137
...this.props,
@@ -1117,11 +1178,12 @@ class VirtualizedList extends React.PureComponent<Props, State> {
11171178
{
11181179
ref: this._captureScrollRef,
11191180
},
1120-
cells,
1181+
this._cells,
11211182
)}
11221183
</VirtualizedListContextProvider>
11231184
);
11241185
let ret = innerRet;
1186+
this._deleteUnusedCells();
11251187
if (__DEV__) {
11261188
ret = (
11271189
<ScrollView.Context.Consumer>
@@ -1931,33 +1993,27 @@ type CellRendererProps = {
19311993
};
19321994

19331995
type CellRendererState = {
1934-
separatorProps: $ReadOnly<{|
1935-
highlighted: boolean,
1936-
leadingItem: ?Item,
1937-
|}>,
1996+
highlighted: boolean,
1997+
leadingItem: ?Item,
19381998
...
19391999
};
19402000

1941-
class CellRenderer extends React.Component<
2001+
class CellRenderer extends React.PureComponent<
19422002
CellRendererProps,
19432003
CellRendererState,
19442004
> {
19452005
state = {
1946-
separatorProps: {
1947-
highlighted: false,
1948-
leadingItem: this.props.item,
1949-
},
2006+
highlighted: false,
2007+
leadingItem: this.props.item,
19502008
};
19512009

19522010
static getDerivedStateFromProps(
19532011
props: CellRendererProps,
19542012
prevState: CellRendererState,
19552013
): ?CellRendererState {
19562014
return {
1957-
separatorProps: {
1958-
...prevState.separatorProps,
1959-
leadingItem: props.item,
1960-
},
2015+
...prevState,
2016+
leadingItem: props.item,
19612017
};
19622018
}
19632019

@@ -1987,7 +2043,8 @@ class CellRenderer extends React.Component<
19872043

19882044
updateSeparatorProps(newProps: Object) {
19892045
this.setState(state => ({
1990-
separatorProps: {...state.separatorProps, ...newProps},
2046+
...state,
2047+
...newProps,
19912048
}));
19922049
}
19932050

@@ -2060,7 +2117,7 @@ class CellRenderer extends React.Component<
20602117
// NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and
20612118
// called explicitly by `ScrollViewStickyHeader`.
20622119
const itemSeparator = ItemSeparatorComponent && (
2063-
<ItemSeparatorComponent {...this.state.separatorProps} />
2120+
<ItemSeparatorComponent {...this.state} />
20642121
);
20652122
const cellStyle = inversionStyle
20662123
? horizontal

0 commit comments

Comments
 (0)