mirror of https://github.com/vector-im/riot-web
Merge pull request #2629 from matrix-org/bwindels/lazyroomtilerendering
Improve room list rendering performancepull/21833/head
commit
90667d8061
|
@ -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 }
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue