Merge pull request #2629 from matrix-org/bwindels/lazyroomtilerendering

Improve room list rendering performance
This commit is contained in:
Bruno Windels 2019-02-13 18:55:47 +01:00 committed by GitHub
commit 90667d8061
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 151 additions and 23 deletions

View File

@ -114,10 +114,15 @@ export default class AutoHideScrollbar extends React.Component {
} }
} }
getScrollTop() {
return this.containerRef.scrollTop;
}
render() { render() {
return (<div return (<div
ref={this._collectContainerRef} ref={this._collectContainerRef}
className={["mx_AutoHideScrollbar", this.props.className].join(" ")} className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
onScroll={this.props.onScroll}
> >
<div className="mx_AutoHideScrollbar_offset"> <div className="mx_AutoHideScrollbar_offset">
{ this.props.children } { this.props.children }

View File

@ -59,6 +59,10 @@ export default class IndicatorScrollbar extends React.Component {
} }
} }
getScrollTop() {
return this._autoHideScrollbar.getScrollTop();
}
componentWillUnmount() { componentWillUnmount() {
if (this._scrollElement) { if (this._scrollElement) {
this._scrollElement.removeEventListener("scroll", this.checkOverflow); this._scrollElement.removeEventListener("scroll", this.checkOverflow);

View File

@ -27,7 +27,8 @@ import IndicatorScrollbar from './IndicatorScrollbar';
import { KeyCode } from '../../Keyboard'; import { KeyCode } from '../../Keyboard';
import { Group } from 'matrix-js-sdk'; import { Group } from 'matrix-js-sdk';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import RoomTile from "../views/rooms/RoomTile";
import LazyRenderList from "../views/elements/LazyRenderList";
// turn this on for drop & drag console debugging galore // turn this on for drop & drag console debugging galore
const debug = false; const debug = false;
@ -60,6 +61,9 @@ const RoomSubList = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
hidden: this.props.startAsHidden || false, hidden: this.props.startAsHidden || false,
// some values to get LazyRenderList starting
scrollerHeight: 800,
scrollTop: 0,
}; };
}, },
@ -134,24 +138,21 @@ const RoomSubList = React.createClass({
this.setState(this.state); this.setState(this.state);
}, },
makeRoomTiles: function() { makeRoomTile: function(room) {
const RoomTile = sdk.getComponent("rooms.RoomTile"); return <RoomTile
return this.props.list.map((room, index) => { room={room}
return <RoomTile roomSubList={this}
room={room} tagName={this.props.tagName}
roomSubList={this} key={room.roomId}
tagName={this.props.tagName} collapsed={this.props.collapsed || false}
key={room.roomId} unread={Unread.doesRoomHaveUnreadMessages(room)}
collapsed={this.props.collapsed || false} highlight={room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite}
unread={Unread.doesRoomHaveUnreadMessages(room)} notificationCount={room.getUnreadNotificationCount()}
highlight={room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite} isInvite={this.props.isInvite}
notificationCount={room.getUnreadNotificationCount()} refreshSubList={this._updateSubListCount}
isInvite={this.props.isInvite} incomingCall={null}
refreshSubList={this._updateSubListCount} onClick={this.onRoomTileClick}
incomingCall={null} />;
onClick={this.onRoomTileClick}
/>;
});
}, },
_onNotifBadgeClick: function(e) { _onNotifBadgeClick: function(e) {
@ -270,6 +271,29 @@ const RoomSubList = React.createClass({
if (this.refs.subList) { if (this.refs.subList) {
this.refs.subList.style.height = `${height}px`; this.refs.subList.style.height = `${height}px`;
} }
this._updateLazyRenderHeight(height);
},
_updateLazyRenderHeight: function(height) {
this.setState({scrollerHeight: height});
},
_onScroll: function() {
this.setState({scrollTop: this.refs.scroller.getScrollTop()});
},
_getRenderItems: function() {
// try our best to not create a new array
// because LazyRenderList rerender when the items prop
// is not the same object as the previous value
const {list, extraTiles} = this.props;
if (!extraTiles || !extraTiles.length) {
return list;
}
if (!list || list.length) {
return extraTiles;
}
return list.concat(extraTiles);
}, },
render: function() { render: function() {
@ -287,12 +311,15 @@ const RoomSubList = React.createClass({
{this._getHeaderJsx(isCollapsed)} {this._getHeaderJsx(isCollapsed)}
</div>; </div>;
} else { } else {
const tiles = this.makeRoomTiles();
tiles.push(...this.props.extraTiles);
return <div ref="subList" className={subListClasses}> return <div ref="subList" className={subListClasses}>
{this._getHeaderJsx(isCollapsed)} {this._getHeaderJsx(isCollapsed)}
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll"> <IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={ this._onScroll }>
{ tiles } <LazyRenderList
scrollTop={this.state.scrollTop }
height={ this.state.scrollerHeight }
renderItem={ this.makeRoomTile }
itemHeight={34}
items={this._getRenderItems()} />
</IndicatorScrollbar> </IndicatorScrollbar>
</div>; </div>;
} }

View File

@ -0,0 +1,92 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
const OVERFLOW_ITEMS = 20;
const OVERFLOW_MARGIN = 5;
class ItemRange {
constructor(topCount, renderCount, bottomCount) {
this.topCount = topCount;
this.renderCount = renderCount;
this.bottomCount = bottomCount;
}
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>);
}
}