使用 React 的大列表性能

我正在使用 React 实现一个可过滤列表。列表的结构如下图所示。

enter image description here

前提

下面是它应该如何工作的描述:

  • 状态位于最高级别的组件 Search组件中。
  • 国家情况描述如下:
{
visible : boolean,
files : array,
filtered : array,
query : string,
currentlySelectedIndex : integer
}
  • files是一个潜在的非常大的数组,包含文件路径(10000个条目是一个合理的数字)。
  • filtered是用户键入至少2个字符后的筛选数组。我知道它是派生数据,因此可以在状态中存储它,但这是必要的
  • currentlySelectedIndex,它是筛选列表中当前选定元素的索引。

  • 用户在 Input组件中键入2个以上的字母,然后过滤数组,并为过滤数组中的每个条目呈现一个 Result组件

  • 每个 Result组件都显示部分匹配查询的完整路径,并突出显示路径的部分匹配部分。例如,如果用户输入了“ le”,那么 Result 组件的 DOM 应该是这样的:

    <li>this/is/a/fi<strong>le</strong>/path</li>

  • 如果用户按下向上或向下键,而 Input组件的重点是基于 filtered阵列的 currentlySelectedIndex变化。这会导致与索引匹配的 Result组件被标记为选定,从而导致重新呈现

问题

最初,我用一个足够小的 files数组(使用 React 的开发版本)对此进行了测试,结果一切正常。

当我不得不处理大至10000个条目的 files数组时,问题出现了。在输入框中键入2个字母会生成一个大列表,当我按下向上和向下键来导航时,会非常滞后。

起初,我没有为 Result元素定义组件,我只是在运行中列出了 Search组件的每个渲染,就像这样:

results  = this.state.filtered.map(function(file, index) {
var start, end, matchIndex, match = this.state.query;


matchIndex = file.indexOf(match);
start = file.slice(0, matchIndex);
end = file.slice(matchIndex + match.length);


return (
<li onClick={this.handleListClick}
data-path={file}
className={(index === this.state.currentlySelected) ? "valid selected" : "valid"}
key={file} >
{start}
<span className="marked">{match}</span>
{end}
</li>
);
}.bind(this));

可以看出,每次 currentlySelectedIndex更改时,都会导致重新呈现,并且每次都会重新创建列表。我想既然我已经为每个 li元素设置了一个 key值,那么 React 就可以避免重新渲染其他没有 className变化的 li元素,但显然事实并非如此。

最后,我为 Result元素定义了一个类,它显式地检查每个 Result元素是否应该基于以前是否选择过并基于当前用户输入重新呈现:

var ResultItem = React.createClass({
shouldComponentUpdate : function(nextProps) {
if (nextProps.match !== this.props.match) {
return true;
} else {
return (nextProps.selected !== this.props.selected);
}
},
render : function() {
return (
<li onClick={this.props.handleListClick}
data-path={this.props.file}
className={
(this.props.selected) ? "valid selected" : "valid"
}
key={this.props.file} >
{this.props.children}
</li>
);
}
});

现在这个列表就是这样创建的:

results = this.state.filtered.map(function(file, index) {
var start, end, matchIndex, match = this.state.query, selected;


matchIndex = file.indexOf(match);
start = file.slice(0, matchIndex);
end = file.slice(matchIndex + match.length);
selected = (index === this.state.currentlySelected) ? true : false


return (
<ResultItem handleClick={this.handleListClick}
data-path={file}
selected={selected}
key={file}
match={match} >
{start}
<span className="marked">{match}</span>
{end}
</ResultItem>
);
}.bind(this));
}

这使得性能 有点更好,但仍然不够好。问题是,当我测试生产版本的反应的东西工作顺利黄油,没有任何滞后。

底线

React 的开发版本和生产版本之间存在如此明显的差异是否正常?

当我思考 React 如何管理列表时,我是否理解/做错了什么

更新日期14-11-2016

我发现了迈克尔 · 杰克逊的这个演讲,他处理了一个与这个非常相似的问题: https://youtu.be/7S8v8jfLb1Q?t=26m2s

这个解决方案与 AskarovBeknar 的 回答提出的解决方案非常相似

更新14-4-2018

由于这显然是一个很受欢迎的问题,而且自从最初的问题被提出以来,事情已经有了进展,虽然我鼓励你观看上面链接的视频,为了掌握一个虚拟布局,我也鼓励你使用 反应虚拟化库,如果你不想重新发明轮子。

95648 次浏览

First of all, the difference between the development and production version of React is huge because in production there are many bypassed sanity checks (such as prop types verification).

Then, I think you should reconsider using Redux because it would be extremely helpful here for what you need (or any kind of flux implementation). You should definitively take a look at this presentation : Big List High Performance React & Redux.

But before diving into redux, you need to made some ajustements to your React code by splitting your components into smaller components because shouldComponentUpdate will totally bypass the rendering of children, so it's a huge gain.

When you have more granular components, you can handle the state with redux and react-redux to better organize the data flow.

I was recently facing a similar issue when I needed to render one thousand rows and be able to modify each row by editing its content. This mini app displays a list of concerts with potential duplicates concerts and I need to chose for each potential duplicate if I want to mark the potential duplicate as an original concert (not a duplicate) by checking the checkbox, and, if necessary, edit the name of the concert. If I do nothing for a particular potential duplicate item, it will be considered duplicate and will be deleted.

Here is what it looks like :

enter image description here

There are basically 4 mains components (there is only one row here but it's for the sake of the example) :

enter image description here

Here is the full code (working CodePen : Huge List with React & Redux) using redux, react-redux, immutable, reselect and recompose:

const initialState = Immutable.fromJS({ /* See codepen, this is a HUGE list */ })


const types = {
CONCERTS_DEDUP_NAME_CHANGED: 'diggger/concertsDeduplication/CONCERTS_DEDUP_NAME_CHANGED',
CONCERTS_DEDUP_CONCERT_TOGGLED: 'diggger/concertsDeduplication/CONCERTS_DEDUP_CONCERT_TOGGLED',
};


const changeName = (pk, name) => ({
type: types.CONCERTS_DEDUP_NAME_CHANGED,
pk,
name
});


const toggleConcert = (pk, toggled) => ({
type: types.CONCERTS_DEDUP_CONCERT_TOGGLED,
pk,
toggled
});




const reducer = (state = initialState, action = {}) => {
switch (action.type) {
case types.CONCERTS_DEDUP_NAME_CHANGED:
return state
.updateIn(['names', String(action.pk)], () => action.name)
.set('_state', 'not_saved');
case types.CONCERTS_DEDUP_CONCERT_TOGGLED:
return state
.updateIn(['concerts', String(action.pk)], () => action.toggled)
.set('_state', 'not_saved');
default:
return state;
}
};


/* configureStore */
const store = Redux.createStore(
reducer,
initialState
);


/* SELECTORS */


const getDuplicatesGroups = (state) => state.get('duplicatesGroups');


const getDuplicateGroup = (state, name) => state.getIn(['duplicatesGroups', name]);


const getConcerts = (state) => state.get('concerts');


const getNames = (state) => state.get('names');


const getConcertName = (state, pk) => getNames(state).get(String(pk));


const isConcertOriginal = (state, pk) => getConcerts(state).get(String(pk));


const getGroupNames = reselect.createSelector(
getDuplicatesGroups,
(duplicates) => duplicates.flip().toList()
);


const makeGetConcertName = () => reselect.createSelector(
getConcertName,
(name) => name
);


const makeIsConcertOriginal = () => reselect.createSelector(
isConcertOriginal,
(original) => original
);


const makeGetDuplicateGroup = () => reselect.createSelector(
getDuplicateGroup,
(duplicates) => duplicates
);






/* COMPONENTS */


const DuplicatessTableRow = Recompose.onlyUpdateForKeys(['name'])(({ name }) => {
return (
<tr>
<td>{name}</td>
<DuplicatesRowColumn name={name}/>
</tr>
)
});


const PureToggle = Recompose.onlyUpdateForKeys(['toggled'])(({ toggled, ...otherProps }) => (
<input type="checkbox" defaultChecked={toggled} {...otherProps}/>
));




/* CONTAINERS */


let DuplicatesTable = ({ groups }) => {


return (
<div>
<table className="pure-table pure-table-bordered">
<thead>
<tr>
<th>{'Concert'}</th>
<th>{'Duplicates'}</th>
</tr>
</thead>
<tbody>
{groups.map(name => (
<DuplicatesTableRow key={name} name={name} />
))}
</tbody>
</table>
</div>
)


};


DuplicatesTable.propTypes = {
groups: React.PropTypes.instanceOf(Immutable.List),
};


DuplicatesTable = ReactRedux.connect(
(state) => ({
groups: getGroupNames(state),
})
)(DuplicatesTable);




let DuplicatesRowColumn = ({ duplicates }) => (
<td>
<ul>
{duplicates.map(d => (
<DuplicateItem
key={d}
pk={d}/>
))}
</ul>
</td>
);


DuplicatessRowColumn.propTypes = {
duplicates: React.PropTypes.arrayOf(
React.PropTypes.string
)
};


const makeMapStateToProps1 = (_, { name }) => {
const getDuplicateGroup = makeGetDuplicateGroup();
return (state) => ({
duplicates: getDuplicateGroup(state, name)
});
};


DuplicatesRowColumn = ReactRedux.connect(makeMapStateToProps1)(DuplicatesRowColumn);




let DuplicateItem = ({ pk, name, toggled, onToggle, onNameChange }) => {
return (
<li>
<table>
<tbody>
<tr>
<td>{ toggled ? <input type="text" value={name} onChange={(e) => onNameChange(pk, e.target.value)}/> : name }</td>
<td>
<PureToggle toggled={toggled} onChange={(e) => onToggle(pk, e.target.checked)}/>
</td>
</tr>
</tbody>
</table>
</li>
)
}


const makeMapStateToProps2 = (_, { pk }) => {
const getConcertName = makeGetConcertName();
const isConcertOriginal = makeIsConcertOriginal();


return (state) => ({
name: getConcertName(state, pk),
toggled: isConcertOriginal(state, pk)
});
};


DuplicateItem = ReactRedux.connect(
makeMapStateToProps2,
(dispatch) => ({
onNameChange(pk, name) {
dispatch(changeName(pk, name));
},
onToggle(pk, toggled) {
dispatch(toggleConcert(pk, toggled));
}
})
)(DuplicateItem);




const App = () => (
<div style=\{\{ maxWidth: '1200px', margin: 'auto' }}>
<DuplicatesTable />
</div>
)


ReactDOM.render(
<ReactRedux.Provider store={store}>
<App/>
</ReactRedux.Provider>,
document.getElementById('app')
);

Lessons learned by doing this mini app when working with huge dataset

  • React components work best when they are kept small
  • Reselect become very useful to avoid recomputation and keep the same reference object (when using immutable.js) given the same arguments.
  • Create connected component for component that are the closest of the data they need to avoid having component only passing down props that they do not use
  • Usage of fabric function to create mapDispatchToProps when you need only the initial prop given in ownProps is necessary to avoid useless re-rendering
  • React & redux definitively rock together !

My experience with a very similar problem is that react really suffers if there are more than 100-200 or so components in the DOM at once. Even if you are super careful (by setting up all your keys and/or implementing a shouldComponentUpdate method) to only to change one or two components on a re-render, you're still going to be in a world of hurt.

The slow part of react at the moment is when it compares the difference between the virtual DOM and the real DOM. If you have thousands of components but only update a couple, it doesn't matter, react still has a massive difference operation to do between the DOMs.

When I write pages now I try to design them to minimise the number of components, one way to do this when rendering large lists of components is to... well... not render large lists of components.

What I mean is: only render the components you can currently see, render more as you scroll down, you're user isn't likely to scroll down through thousands of components any way.... I hope.

A great library for doing this is:

https://www.npmjs.com/package/react-infinite-scroll

With a great how-to here:

http://www.reactexamples.com/react-infinite-scroll/

I'm afraid it doesn't remove components that are off the top of the page though, so if you scroll for long enough you're performance issues will start to reemerge.

I know it isn't good practice to provide a link as answer, but the examples they provide are going to explain how to use this library much better than I can here. Hopefully I have explained why big lists are bad, but also a work around.

  1. React in development version checks for proptypes of each component to ease development process, while in production it is omitted.

  2. Filtering list of strings is very expensive operation for every keyup. it might cause performance issues because of single threaded nature of JavaScript. Solution might be to use debounce method to delay execution of your filter function until the delay is expired.

  3. Another problem might be the huge list itself. You can create virtual layout and reuse created items just replacing data. Basically you create scrollable container component with fixed height, inside of which you will place list container. The height of list container should be set manually (itemHeight * numberOfItems) depending on the length of visible list, to have a scrollbar working. Then create a few item components so that they will fill scrollable containers height and maybe add extra one or two mimic continuous list effect. make them absolute position and on scroll just move their position so that it will mimic continuous list(I think you will find out how to implement it:)

  4. One more thing is writing to DOM is also expensive operation especially if you do it wrong. You can use canvas for displaying lists and create smooth experience on scroll. Checkout react-canvas components. I heard that they have already done some work on Lists.

Try filter before loading into the React component and only show a reasonable amount of items in the component and load more on demand. Nobody can view that many items at one time.

I don't think you are, but don't use indexes as keys.

To find out the real reason why the development and production versions are different you could try profiling your code.

Load your page, start recording, perform a change, stop recording and then check out the timings. See here for instructions for performance profiling in Chrome.

Like I mentioned in my comment, I doubt that users need all those 10000 results in the browser at once.

What if you page through the results, and always just show a list of 10 results.

I've created an example using this technique, without using any other library like Redux. Currently only with keyboard navigation, but could easily be extended to work on scrolling as well.

The example exists of 3 components, the container application, a search component and a list component. Almost all the logic has been moved to the container component.

The gist lies in keeping track of the start and the selected result, and shifting those on keyboard interaction.

nextResult: function() {
var selected = this.state.selected + 1
var start = this.state.start
if(selected >= start + this.props.limit) {
++start
}
if(selected + start < this.state.results.length) {
this.setState({selected: selected, start: start})
}
},


prevResult: function() {
var selected = this.state.selected - 1
var start = this.state.start
if(selected < start) {
--start
}
if(selected + start >= 0) {
this.setState({selected: selected, start: start})
}
},

While simply passing all the files through a filter:

updateResults: function() {
var results = this.props.files.filter(function(file){
return file.file.indexOf(this.state.query) > -1
}, this)


this.setState({
results: results
});
},

And slicing the results based on start and limit in the render method:

render: function() {
var files = this.state.results.slice(this.state.start, this.state.start + this.props.limit)
return (
<div>
<Search onSearch={this.onSearch} onKeyDown={this.onKeyDown} />
<List files={files} selected={this.state.selected - this.state.start} />
</div>
)
}

Fiddle containing a full working example: https://jsfiddle.net/koenpunt/hm1xnpqk/

As with many of the other answers to this question the main problem lies in the fact that rendering so many elements in the DOM whilst doing filtering and handling key events is going to be slow.

You are not doing anything inherently wrong with regards to React that is causing the issue but like many of the issues that are performance related the UI can also take a big percentage of the blame.

If your UI is not designed with efficiency in mind even tools like React that are designed to be performant will suffer.

Filtering the result set is a great start as mentioned by @Koen

I've played around with the idea a bit and created an example app illustrating how I might start to tackle this kind of problem.

This is by no means production ready code but it does illustrate the concept adequately and can be modified to be more robust, feel free to take a look at the code - I hope at the very least it gives you some ideas...;)

react-large-list-example

enter image description here

Check out React Virtualized Select, it's designed to address this issue and performs impressively in my experience. From the description:

HOC that uses react-virtualized and react-select to display large lists of options in a drop-down

https://github.com/bvaughn/react-virtualized-select

For anyone struggling with this problem I have written a component react-big-list that handles lists to up to 1 million of records.

On top of that it comes with some fancy extra features like:

  • Sorting
  • Caching
  • Custom filtering
  • ...

We are using it in production in quite some apps and it works great.

React has recommend react-window library: https://www.npmjs.com/package/react-window

It better than react-vitualized. You can try it

I recently developed multi-select input for React and tested it with 48.000 records. It's working without any problem.

https://www.npmjs.com/package/react-multi-select-advanced

Here's my attempt on this

react-async-lazy-list OR, https://github.com/sawrozpdl/react-async-lazy-list

you can give it a shot. it's pretty fast as it uses windowing/virtualization and comes with lazy loading and full customization.