Optimize WindowedListView

Summary:
Reduce re-renders by only looking at `props.data` that we're actually going to render and tracking if `this._rowFrames`
is dirty.

Differential Revision: D3195163

fb-gh-sync-id: 1e17ab410a312a37d4a93b84ea51ca32c3ede839
fbshipit-source-id: 1e17ab410a312a37d4a93b84ea51ca32c3ede839
This commit is contained in:
Spencer Ahrens 2016-04-29 10:14:54 -07:00 committed by Facebook Github Bot 5
parent 856f9e9fed
commit 6cae8b7c02
1 changed files with 50 additions and 33 deletions

View File

@ -165,15 +165,16 @@ type State = {
boundaryIndicatorHeight?: number;
firstRow: number;
lastRow: number;
firstVisible: number;
lastVisible: number;
};
class WindowedListView extends React.Component {
props: Props;
state: State;
_firstVisible: number = -1;
_lastVisible: number = -1;
_scrollOffsetY: number = 0;
_frameHeight: number = 0;
_rowFrames: Array<Object> = [];
_rowFramesDirty: boolean = false;
_hasCalledOnEndReached: bool = false;
_willComputeRowsToRender: bool = false;
_timeoutHandle: number = 0;
@ -201,10 +202,7 @@ class WindowedListView extends React.Component {
);
this.state = {
firstRow: 0,
lastRow:
Math.min(this.props.data.length, this.props.initialNumToRender) - 1,
firstVisible: -1,
lastVisible: -1,
lastRow: Math.min(this.props.data.length, this.props.initialNumToRender) - 1,
};
}
getScrollResponder(): ?ReactComponent {
@ -212,6 +210,27 @@ class WindowedListView extends React.Component {
this._scrollRef.getScrollResponder &&
this._scrollRef.getScrollResponder();
}
shouldComponentUpdate(newProps: Props, newState: State): boolean {
if (newState !== this.state) {
return true;
}
for (const key in newProps) {
if (key !== 'data' && newProps[key] !== this.props[key]) {
return true;
}
}
const newDataSubset = newProps.data.slice(newState.firstRow, newState.lastRow + 1);
const prevDataSubset = this.props.data.slice(this.state.firstRow, this.state.lastRow + 1);
if (newDataSubset.length !== prevDataSubset.length) {
return true;
}
for (let idx = 0; idx < newDataSubset.length; idx++) {
if (newDataSubset[idx] !== prevDataSubset[idx]) {
return true;
}
}
return false;
}
componentWillReceiveProps(newProps: Object) {
// This has to happen immediately otherwise we could crash, e.g. if the data
// array has gotten shorter.
@ -253,6 +272,7 @@ class WindowedListView extends React.Component {
);
}
this._rowFrames[rowIndex] = {...layout, offscreenLayoutDone: true};
this._rowFramesDirty = true;
if (this._cellsInProgress.size === 0) {
this._enqueueComputeRowsToRender();
}
@ -297,11 +317,10 @@ class WindowedListView extends React.Component {
_computeRowsToRender(props: Object): void {
const totalRows = props.data.length;
if (totalRows === 0) {
this._updateVisibleRows(-1, -1);
this.setState({
firstRow: 0,
lastRow: -1,
firstVisible: -1,
lastVisible: -1,
});
return;
}
@ -376,34 +395,28 @@ class WindowedListView extends React.Component {
this._hasCalledOnEndReached = this.state.lastRow === lastRow;
}
}
this.setState({firstRow, lastRow});
}
componentDidUpdate(prevProps: Props, prevState: State) {
const {firstRow, lastRow} = this.state;
if (firstRow !== prevState.firstRow || lastRow !== prevState.lastRow) {
this.props.onMountedRowsWillChange && this.props.onMountedRowsWillChange(firstRow, lastRow - firstRow + 1);
console.log('WLV: row render range changed:', {firstRow, lastRow});
}
if (this.props.onVisibleRowsChanged) {
const {firstVisible, lastVisible} = this.state;
if (firstVisible !== prevState.firstVisible ||
lastVisible !== prevState.lastVisible) {
this.props.onVisibleRowsChanged(firstVisible, lastVisible - lastVisible + 1);
const rowsShouldChange = firstRow !== this.state.firstRow || lastRow !== this.state.lastRow;
if (this._rowFramesDirty || rowsShouldChange) {
if (rowsShouldChange) {
this.props.onMountedRowsWillChange && this.props.onMountedRowsWillChange(firstRow, lastRow - firstRow + 1);
console.log('WLV: row render range will change:', {firstRow, lastRow});
}
this._rowFramesDirty = false;
this.setState({firstRow, lastRow});
}
}
_updateVisibleRows(newFirstVisible: number, newLastVisible: number) {
if (this.state.firstVisible !== newFirstVisible ||
this.state.lastVisible !== newLastVisible) {
this.setState({
firstVisible: newFirstVisible,
lastVisible: newLastVisible,
});
if (this.props.onVisibleRowsChanged) {
if (this._firstVisible !== newFirstVisible ||
this._lastVisible !== newLastVisible) {
this.props.onVisibleRowsChanged(newFirstVisible, newLastVisible - newFirstVisible + 1);
}
}
this._firstVisible = newFirstVisible;
this._lastVisible = newLastVisible;
}
render(): ReactElement {
const firstRow = this.state.firstRow;
const lastRow = this.state.lastRow;
const {firstRow, lastRow} = this.state;
const rowFrames = this._rowFrames;
const rows = [];
let spacerHeight = 0;
@ -452,7 +465,8 @@ class WindowedListView extends React.Component {
rowIndex={idx}
onNewLayout={this._onNewLayout}
onWillUnmount={this._onWillUnmountCell}
includeInLayout={this._rowFrames[idx] && this._rowFrames[idx].offscreenLayoutDone}
includeInLayout={this.props.disableIncrementalRendering ||
(this._rowFrames[idx] && this._rowFrames[idx].offscreenLayoutDone)}
onProgressChange={this._onProgressChange}
asyncRowPerfEventName={this.props.asyncRowPerfEventName}
data={this.props.data[idx]}
@ -556,12 +570,16 @@ class CellRenderer extends React.Component {
_lastLayout: ?Object = null;
_perfUpdateID: number = 0;
_asyncCookie: any;
_includeInLayoutLatch: boolean = false;
componentWillMount() {
if (this.props.asyncRowPerfEventName) {
this._perfUpdateID = g_perf_update_id++;
this._asyncCookie = Systrace.beginAsyncEvent(this.props.asyncRowPerfEventName + this._perfUpdateID);
console.log(`perf_asynctest_${this.props.asyncRowPerfEventName}_start ${this._perfUpdateID} ${Date.now()}`);
}
if (this.props.includeInLayout) {
this._includeInLayoutLatch = true;
}
this.props.onProgressChange({rowIndex: this.props.rowIndex, inProgress: true});
}
_onLayout = (e) => {
@ -605,9 +623,8 @@ class CellRenderer extends React.Component {
componentWillReceiveProps(newProps) {
if (newProps.includeInLayout && !this.props.includeInLayout) {
invariant(this._offscreenRenderDone, 'Should never try to add to layout before render done');
this._includeInLayoutLatch = true; // Once we render in layout, make sure it sticks.
this.refs.container.setNativeProps({style: styles.include});
} else {
invariant(!(this.props.includeInLayout && !newProps.includeInLayout), 'Should never unset includeInLayout');
}
}
shouldComponentUpdate(newProps) {
@ -622,7 +639,7 @@ class CellRenderer extends React.Component {
Row: {this.props.rowIndex}
</Text>;
}
const style = this.props.includeInLayout ? styles.include : styles.remove;
const style = (this._includeInLayoutLatch || this.props.includeInLayout) ? styles.include : styles.remove;
return (
<IncrementalGroup onDone={this._onOffscreenRenderDone} name={`CellRenderer_${this.props.rowIndex}`}>
<View