From 1b5e0682464a1e6d3a5ed31172dc54e72d2f824e Mon Sep 17 00:00:00 2001 From: Alex Vertin Date: Fri, 5 Feb 2021 15:44:49 -0800 Subject: [PATCH] Perf optimization: dont render offscreen items --- lib/AutoDragSortableView.js | 119 +++++++++++++++++++++++++++++++----- lib/index.d.ts | 2 + 2 files changed, 107 insertions(+), 14 deletions(-) diff --git a/lib/AutoDragSortableView.js b/lib/AutoDragSortableView.js index 6e146d3..dd5023b 100644 --- a/lib/AutoDragSortableView.js +++ b/lib/AutoDragSortableView.js @@ -7,6 +7,10 @@ const {width,height} = Dimensions.get('window') const defaultZIndex = 8 const touchZIndex = 99 +function determineInitialItemRenderCountForOptimization(itemHeight, columnCount) { + return Math.max(Math.ceil(Dimensions.get("window").height / itemHeight) * columnCount, 15); +} + export default class AutoDragSortableView extends Component{ constructor(props) { @@ -20,6 +24,9 @@ export default class AutoDragSortableView extends Component{ // this.reComplexDataSource(true,props) // react < 16.3 // react > 16.3 Fiber const rowNum = parseInt(props.parentWidth / itemWidth); + + const forLoadOptimizationInitialRenderCount = determineInitialItemRenderCountForOptimization(itemHeight, rowNum); + const dataSource = props.dataSource.map((item, index) => { const newData = {} const left = (index % rowNum) * itemWidth @@ -34,16 +41,25 @@ export default class AutoDragSortableView extends Component{ y: parseInt(top + 0.5), }) newData.scaleValue = new Animated.Value(1) + newData.shouldRender = props.enableInitialLoadOptimization ? index < forLoadOptimizationInitialRenderCount : true; return newData }); this.state = { dataSource: dataSource, curPropsDataSource: props.dataSource, height: Math.ceil(dataSource.length / rowNum) * itemHeight, + columnCount: rowNum, + rowCount: Math.ceil(dataSource.length / rowNum), itemWidth, itemHeight, }; + this.viewableItemsData = { + layoutDimensions: undefined, + mostRecentStartItemIndex: undefined, + mostRecentEndItemIndex: undefined, + }; + this._panResponder = PanResponder.create({ onStartShouldSetPanResponder: (evt, gestureState) => true, onStartShouldSetPanResponderCapture: (evt, gestureState) => { @@ -76,6 +92,7 @@ export default class AutoDragSortableView extends Component{ if (nextprops.dataSource != prevState.curPropsDataSource || itemWidth !== prevState.itemWidth || itemHeight !== prevState.itemHeight) { const rowNum = parseInt(nextprops.parentWidth / itemWidth); + const forLoadOptimizationInitialRenderCount = determineInitialItemRenderCountForOptimization(itemHeight, rowNum); const dataSource = nextprops.dataSource.map((item, index) => { const newData = {}; const left = index % rowNum * itemWidth; @@ -90,12 +107,17 @@ export default class AutoDragSortableView extends Component{ y: parseInt(top + 0.5), }); newData.scaleValue = new Animated.Value(1); + newData.shouldRender = nextprops.enableInitialLoadOptimization ? + (prevState.dataSource[index] !== undefined ? prevState.dataSource[index].shouldRender : index < forLoadOptimizationInitialRenderCount) : + true; return newData; }); return { dataSource: dataSource, curPropsDataSource: nextprops.dataSource, height: Math.ceil(dataSource.length / rowNum) * itemHeight, + columnCount: rowNum, + rowCount: Math.ceil(dataSource.length / rowNum), itemWidth, itemHeight, } @@ -110,6 +132,11 @@ export default class AutoDragSortableView extends Component{ componentDidUpdate() { this.autoMeasureHeight() + if (this.state.columnCount !== prevState.columnCount || this.state.rowCount !== prevState.rowCount || this.state.dataSource !== prevState.dataSource) { + this.viewableItemsData.mostRecentStartItemIndex = undefined; + this.viewableItemsData.mostRecentEndItemIndex = undefined; + } + this.updateViewableItems(); } // Compatible with different systems and paging loading @@ -582,10 +609,68 @@ export default class AutoDragSortableView extends Component{ offsetY: nativeEvent.contentOffset.y, hasScroll: true, } + this.updateViewableItems(); if (nativeEvent.contentOffset.y !== 0) this.isHasMeasure = true; if (this.props.onScrollListener) this.props.onScrollListener(event) } + handleLayout = ({ nativeEvent}) => { + this.viewableItemsData.layoutDimensions = nativeEvent.layout; + this.updateViewableItems(); + } + + updateViewableItems = () => { + if (!this.props.enableInitialLoadOptimization) { + return; + } + const windowHeight = this.curScrollData !== undefined ? this.curScrollData.windowHeight : (this.viewableItemsData.layoutDimensions !== undefined ? this.viewableItemsData.layoutDimensions.height : 0); + const rowHeight = this.state.itemHeight; + const maxRowsVisibleOnScreen = (1 + Math.ceil(windowHeight / rowHeight)); + + const offsetY = this.curScrollData !== undefined ? this.curScrollData.offsetY : 0; + + // In the following two calculations, think of maxRowsVisibleOnScreen as + // like "page size" or "batch size" (where a batch or page is measured in # + // of rows, not items) + const topRowIndex = Math.floor(Math.floor(offsetY / rowHeight) / maxRowsVisibleOnScreen) * maxRowsVisibleOnScreen; + // The `+ (maxRowsVisibleOnScreen*2)` means we render EVEN MORE than what + // is visible (like an additional "batch") + const bottomRowIndex = Math.ceil(Math.ceil((offsetY + windowHeight) / rowHeight) / maxRowsVisibleOnScreen) * maxRowsVisibleOnScreen + (maxRowsVisibleOnScreen*2); + + const startItemIndex = Math.max(topRowIndex, 0) * this.state.columnCount; + const endItemIndex = Math.min(bottomRowIndex * this.state.columnCount + this.state.columnCount - 1, this.state.dataSource.length - 1); + + // This is a small perf optimization to avoid going through the loop if we just did these rows + if (startItemIndex === this.viewableItemsData.mostRecentStartItemIndex && endItemIndex === this.viewableItemsData.mostRecentEndItemIndex) { + // No need to do any updates + return; + } + + this.viewableItemsData.mostRecentStartItemIndex = startItemIndex; + this.viewableItemsData.mostRecentEndItemIndex = endItemIndex; + + let changed = false; + + for (let i = startItemIndex; i <= endItemIndex; i++) { + const item = this.state.dataSource[i]; + if (item.shouldRender) { + continue; + } + item.shouldRender = true; + changed = true; + } + + if (changed) { + // This looks like it doesn't do anything but actually it triggers a + // re-render since this component isn't a PureComponent. The reason we + // do this is because the update to `shouldRender` above won't by + // itself trigger a re-render because React has no idea that some + // object nested within another object that is part of state has + // changed. + this.setState({ dataSource: this.state.dataSource }) + } + } + render() { return ( + style={styles.container} + onLayout={this.handleLayout} + > {this.props.renderHeaderView ? this.props.renderHeaderView : null} this.sortParentRef=ref} @@ -644,19 +731,21 @@ export default class AutoDragSortableView extends Component{ opacity: item.scaleValue.interpolate({inputRange,outputRange}), transform: [transformObj] }]}> - this.onPressOut()} - onLongPress={()=>this.startTouch(index)} - onPress={()=>{ - if (this.props.onClickItem) { - this.isHasMeasure = true - this.props.onClickItem(this.getOriginalData(),item.data,index) - } - }}> - {this.props.renderItem(item.data,index)} - + {(!this.props.enableInitialLoadOptimization || item.shouldRender) && + this.onPressOut()} + onLongPress={()=>this.startTouch(index)} + onPress={()=>{ + if (this.props.onClickItem) { + this.isHasMeasure = true + this.props.onClickItem(this.getOriginalData(),item.data,index) + } + }}> + {this.props.renderItem(item.data,index)} + + } ) }) @@ -682,6 +771,8 @@ AutoDragSortableView.propTypes = { sortable: PropTypes.bool, + enableInitialLoadOptimization: PropTypes.bool, + onClickItem: PropTypes.func, onDragStart: PropTypes.func, onDragEnd: PropTypes.func, diff --git a/lib/index.d.ts b/lib/index.d.ts index f270f76..b6051fc 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -14,6 +14,8 @@ interface IProps{ sortable?: boolean; + enableInitialLoadOptimization?: boolean; + onClickItem?: (data: any[],item: any,index: number) => void; onDragStart?: (fromIndex: number) => void; onDragEnd?: (fromIndex: number,toIndex: number) => void;