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