Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Merge pull request #2629 from matrix-org/bwindels/lazyroomtilerendering
Browse files Browse the repository at this point in the history
Improve room list rendering performance
  • Loading branch information
bwindels authored Feb 13, 2019
2 parents 694a59a + e51f279 commit 90667d8
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 23 deletions.
5 changes: 5 additions & 0 deletions src/components/structures/AutoHideScrollbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,15 @@ export default class AutoHideScrollbar extends React.Component {
}
}

getScrollTop() {
return this.containerRef.scrollTop;
}

render() {
return (<div
ref={this._collectContainerRef}
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
onScroll={this.props.onScroll}
>
<div className="mx_AutoHideScrollbar_offset">
{ this.props.children }
Expand Down
4 changes: 4 additions & 0 deletions src/components/structures/IndicatorScrollbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export default class IndicatorScrollbar extends React.Component {
}
}

getScrollTop() {
return this._autoHideScrollbar.getScrollTop();
}

componentWillUnmount() {
if (this._scrollElement) {
this._scrollElement.removeEventListener("scroll", this.checkOverflow);
Expand Down
73 changes: 50 additions & 23 deletions src/components/structures/RoomSubList.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import IndicatorScrollbar from './IndicatorScrollbar';
import { KeyCode } from '../../Keyboard';
import { Group } from 'matrix-js-sdk';
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
const debug = false;
Expand Down Expand Up @@ -60,6 +61,9 @@ const RoomSubList = React.createClass({
getInitialState: function() {
return {
hidden: this.props.startAsHidden || false,
// some values to get LazyRenderList starting
scrollerHeight: 800,
scrollTop: 0,
};
},

Expand Down Expand Up @@ -134,24 +138,21 @@ const RoomSubList = React.createClass({
this.setState(this.state);
},

makeRoomTiles: function() {
const RoomTile = sdk.getComponent("rooms.RoomTile");
return this.props.list.map((room, index) => {
return <RoomTile
room={room}
roomSubList={this}
tagName={this.props.tagName}
key={room.roomId}
collapsed={this.props.collapsed || false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
highlight={room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite}
notificationCount={room.getUnreadNotificationCount()}
isInvite={this.props.isInvite}
refreshSubList={this._updateSubListCount}
incomingCall={null}
onClick={this.onRoomTileClick}
/>;
});
makeRoomTile: function(room) {
return <RoomTile
room={room}
roomSubList={this}
tagName={this.props.tagName}
key={room.roomId}
collapsed={this.props.collapsed || false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
highlight={room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite}
notificationCount={room.getUnreadNotificationCount()}
isInvite={this.props.isInvite}
refreshSubList={this._updateSubListCount}
incomingCall={null}
onClick={this.onRoomTileClick}
/>;
},

_onNotifBadgeClick: function(e) {
Expand Down Expand Up @@ -270,6 +271,29 @@ const RoomSubList = React.createClass({
if (this.refs.subList) {
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() {
Expand All @@ -287,12 +311,15 @@ const RoomSubList = React.createClass({
{this._getHeaderJsx(isCollapsed)}
</div>;
} else {
const tiles = this.makeRoomTiles();
tiles.push(...this.props.extraTiles);
return <div ref="subList" className={subListClasses}>
{this._getHeaderJsx(isCollapsed)}
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll">
{ tiles }
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={ this._onScroll }>
<LazyRenderList
scrollTop={this.state.scrollTop }
height={ this.state.scrollerHeight }
renderItem={ this.makeRoomTile }
itemHeight={34}
items={this._getRenderItems()} />
</IndicatorScrollbar>
</div>;
}
Expand Down
92 changes: 92 additions & 0 deletions src/components/views/elements/LazyRenderList.js
Original file line number Diff line number Diff line change
@@ -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>);
}
}

0 comments on commit 90667d8

Please sign in to comment.