make LazyRenderList stateful for better performance

it only rerenders when visible range it would render based on the
props gets OVERFLOW_MARGIN(5) items from the current renderRange
This commit is contained in:
Bruno Windels 2019-02-13 17:16:20 +01:00
parent 9c371111b7
commit 60d0ed4c01
2 changed files with 81 additions and 22 deletions

View file

@ -61,14 +61,14 @@ const RoomSubList = React.createClass({
getInitialState: function() {
// throttle updates to LazyRenderList
this._onScroll = _.throttle(
this._onScroll, 100,
{leading: false, trailing: true},
);
this._updateLazyRenderHeight = _.throttle(
this._updateLazyRenderHeight, 100,
{leading: false, trailing: true},
);
// this._onScroll = _.throttle(
// this._onScroll, 50,
// {leading: false, trailing: true},
// );
// this._updateLazyRenderHeight = _.throttle(
// this._updateLazyRenderHeight, 100,
// {leading: false, trailing: true},
// );
return {
hidden: this.props.startAsHidden || false,
// some values to get LazyRenderList starting
@ -307,7 +307,7 @@ const RoomSubList = React.createClass({
{this._getHeaderJsx(isCollapsed)}
</div>;
} else {
const items = this.props.list.concat(this.props.extraTiles);
const items = this.props.list; //.concat(this.props.extraTiles);
return <div ref="subList" className={subListClasses}>
{this._getHeaderJsx(isCollapsed)}
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={ this._onScroll }>

View file

@ -14,20 +14,79 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const OVERFLOW_ITEMS = 2;
import React from "react";
export default function LazyRenderList(props) {
const {items, itemHeight, scrollTop, height, renderItem} = props;
const OVERFLOW_ITEMS = 20;
const OVERFLOW_MARGIN = 5;
const firstIdx = Math.max(0, Math.floor(scrollTop / itemHeight) - OVERFLOW_ITEMS);
const itemsAfterFirst = items.length - firstIdx;
const amount = Math.min(Math.ceil(height / itemHeight) + OVERFLOW_ITEMS, itemsAfterFirst);
const beforeSpace = firstIdx * itemHeight;
const itemsAfter = itemsAfterFirst - amount;
const afterSpace = itemsAfter * itemHeight;
const renderedItems = items.slice(firstIdx, firstIdx + amount);
class ItemRange {
constructor(topCount, renderCount, bottomCount) {
this.topCount = topCount;
this.renderCount = renderCount;
this.bottomCount = bottomCount;
}
return (<div style={{paddingTop: `${beforeSpace}px`, paddingBottom: `${afterSpace}px`}}>
{ renderedItems.map(renderItem) }
</div>);
contains(range) {
return range.topCount >= this.topCount &&
(range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
}
expand(amount) {
const topGrow = Math.min(amount, this.topCount);
const bottomGrow = Math.min(amount, this.bottomCount);
return new ItemRange(
this.topCount - topGrow,
this.renderCount + topGrow + bottomGrow,
this.bottomCount - bottomGrow,
);
}
}
export default class LazyRenderList extends React.Component {
constructor(props) {
super(props);
const renderRange = LazyRenderList.getVisibleRangeFromProps(props).expand(OVERFLOW_ITEMS);
this.state = {renderRange};
}
static getVisibleRangeFromProps(props) {
const {items, itemHeight, scrollTop, height} = props;
const length = items ? items.length : 0;
const topCount = Math.max(0, Math.floor(scrollTop / itemHeight));
const itemsAfterTop = length - topCount;
const renderCount = Math.min(Math.ceil(height / itemHeight), itemsAfterTop);
const bottomCount = itemsAfterTop - renderCount;
return new ItemRange(topCount, renderCount, bottomCount);
}
componentWillReceiveProps(props) {
const state = this.state;
const range = LazyRenderList.getVisibleRangeFromProps(props);
// only update state if the new range isn't contained by the old anymore
if (!state.renderRange || !state.renderRange.contains(range.expand(OVERFLOW_MARGIN))) {
this.setState({renderRange: range.expand(OVERFLOW_ITEMS)});
}
}
shouldComponentUpdate(nextProps, nextState) {
const itemsChanged = nextProps.items !== this.props.items;
const rangeChanged = nextState.renderRange !== this.state.renderRange;
return itemsChanged || rangeChanged;
}
render() {
const {itemHeight, items, renderItem} = this.props;
const {renderRange} = this.state;
const paddingTop = renderRange.topCount * itemHeight;
const paddingBottom = renderRange.bottomCount * itemHeight;
const renderedItems = items.slice(
renderRange.topCount,
renderRange.topCount + renderRange.renderCount,
);
return (<div style={{paddingTop: `${paddingTop}px`, paddingBottom: `${paddingBottom}px`}}>
{ renderedItems.map(renderItem) }
</div>);
}
}