javascript React 性能:使用 PureRenderMixin 渲染大列表
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/30976722/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
React performance: rendering big list with PureRenderMixin
提问by Sebastien Lorber
I took a TodoList example to reflect my problem but obviously my real-world code is more complex.
我拿了一个 TodoList 例子来反映我的问题,但显然我的实际代码更复杂。
I have some pseudo-code like this.
我有一些这样的伪代码。
var Todo = React.createClass({
mixins: [PureRenderMixin],
............
}
var TodosContainer = React.createClass({
mixins: [PureRenderMixin],
renderTodo: function(todo) {
return <Todo key={todo.id} todoData={todo} x={this.props.x} y={this.props.y} .../>;
},
render: function() {
var todos = this.props.todos.map(this.renderTodo)
return (
<ReactCSSTransitionGroup transitionName="transition-todo">
{todos}
</ReactCSSTransitionGroup>,
);
}
});
All my data is immutable, and PureRenderMixin is used appropriately and everything works fine. When a Todo data is modified, only the parent and the edited todo is re-rendered.
我所有的数据都是不可变的,并且 PureRenderMixin 使用得当,一切正常。当一个 Todo 数据被修改时,只有父级和编辑过的 todo 被重新渲染。
The problem is that at some point my list grows big as the user is scrolling. And when a single Todo is updated, it takes more and more time to render the parent, call shouldComponentUpdate
on all the todos, and then render the single todo.
问题是在某些时候我的列表会随着用户滚动而变大。并且当单个 Todo 更新时,渲染父级、调用shouldComponentUpdate
所有Todo ,然后渲染单个 Todo 所花费的时间越来越长。
As you can see, the Todo component has other component than the Todo data. This is data that is required for render by all the todos and is shared (for example we could imagine there's a "displayMode" for the todos). Having many properties makes the shouldComponentUpdate
perform a little bit slower.
如您所见,Todo 组件具有除 Todo 数据之外的其他组件。这是所有待办事项渲染所需并共享的数据(例如,我们可以想象待办事项有一个“显示模式”)。拥有许多属性会使shouldComponentUpdate
执行速度变慢。
Also, using ReactCSSTransitionGroup
seems to slow down a little too, as ReactCSSTransitionGroup
have to render itself and ReactCSSTransitionGroupChild
even before the shouldComponentUpdate
of todos is called. React.addons.Perf
shows that ReactCSSTransitionGroup > ReactCSSTransitionGroupChild
rendering is time wasted for each item of the list.
此外,使用ReactCSSTransitionGroup
似乎也有点慢,因为ReactCSSTransitionGroup
必须渲染自己, ReactCSSTransitionGroupChild
甚至在shouldComponentUpdate
调用 todos之前。React.addons.Perf
显示ReactCSSTransitionGroup > ReactCSSTransitionGroupChild
渲染是浪费时间在列表中的每个项目上。
So, as far as I know, I use PureRenderMixin
but with a larger list this may be not enough. I still get not so bad performances, but would like to know if there are easy ways to optimize my renderings.
所以,据我所知,我使用PureRenderMixin
了更大的列表,这可能还不够。我的表现仍然不那么糟糕,但想知道是否有简单的方法来优化我的渲染。
Any idea?
任何的想法?
Edit:
编辑:
So far, my big list is paginated, so instead of having a big list of items, I now split this big list in a list of pages. This permits to have better performances as each page can now implement shouldComponentUpdate
. Now when an item changes in a page, React only has to call the main render function that iterates on the page, and only call the render function from a single page, which make a lot less iteration work.
到目前为止,我的大列表是分页的,所以我现在将这个大列表拆分为一个页面列表,而不是一个大的项目列表。这允许有更好的性能,因为每个页面现在都可以实现shouldComponentUpdate
。现在当一个页面中的一个item发生变化时,React只需要调用在页面上迭代的主render函数,并且只从单个页面调用render函数,这样迭代工作就少了很多。
However, my render performance is still linear to the page number (O(n)) I have. So if I have thousands of pages it's still the same issue :) In my usecase it's unlikely to happen but I'm still interested in a better solution.
但是,我的渲染性能仍然与我拥有的页码 (O(n)) 呈线性关系。因此,如果我有数千页,它仍然是相同的问题 :) 在我的用例中,它不太可能发生,但我仍然对更好的解决方案感兴趣。
I am pretty sure it is possible to achieve O(log(n)) rendering performance where n is the number of items (or pages), by splitting a large list into a tree (like a persistent data structure), and where each node has the power to short-circuit the computation with shouldComponentUpdate
我很确定可以实现 O(log(n)) 渲染性能,其中 n 是项目(或页面)的数量,通过将大列表拆分为树(如持久数据结构),以及每个节点的位置有能力使计算短路 shouldComponentUpdate
Yes I'm thinking of something akin to persistent data structures like Vector in Scala or Clojure:
是的,我正在考虑类似于 Scala 或 Clojure 中的 Vector 之类的持久数据结构:
However I'm concerned about React because as far as I know it may have to create intermediate dom nodes when rendering the internal nodes of the tree. This may be a problem according to the usecase (and may be solved in future versions of React)
但是,我担心 React,因为据我所知,在渲染树的内部节点时,它可能必须创建中间 dom 节点。根据用例,这可能是一个问题(可能会在 React 的未来版本中解决)
Also as we are using Javascript I wonder if Immutable-JS support this, and make the "internal nodes" accessible. See: https://github.com/facebook/immutable-js/issues/541
此外,当我们使用 Javascript 时,我想知道 Immutable-JS 是否支持这一点,并使“内部节点”可访问。见:https: //github.com/facebook/immutable-js/issues/541
Edit: useful link with my experiments: Can a React-Redux app really scale as well as, say Backbone? Even with reselect. On mobile
编辑:与我的实验的有用链接:React-Redux 应用程序真的可以像 Backbone 一样扩展吗?即使重新选择。在移动
采纳答案by Sebastien Lorber
Here is a POC implementation I've done with ImmutableJS internal structure. This is not a public API so it is not ready for production and does not currently handle corner cases but it works.
这是我使用 ImmutableJS 内部结构完成的 POC 实现。这不是公共 API,因此尚未准备好用于生产,并且当前无法处理极端情况,但它可以工作。
var ImmutableListRenderer = React.createClass({
render: function() {
// Should not require to use wrapper <span> here but impossible for now
return (<span>
{this.props.list._root ? <GnRenderer gn={this.props.list._root}/> : undefined}
{this.props.list._tail ? <GnRenderer gn={this.props.list._tail}/> : undefined}
</span>);
}
})
// "Gn" is the equivalent of the "internal node" of the persistent data structure schema of the question
var GnRenderer = React.createClass({
shouldComponentUpdate: function(nextProps) {
console.debug("should update?",(nextProps.gn !== this.props.gn));
return (nextProps.gn !== this.props.gn);
},
propTypes: {
gn: React.PropTypes.object.isRequired,
},
render: function() {
// Should not require to use wrapper <span> here but impossible for now
return (
<span>
{this.props.gn.array.map(function(gnItem,index) {
// TODO should check for Gn instead, because list items can be objects too...
var isGn = typeof gnItem === "object"
if ( isGn ) {
return <GnRenderer gn={gnItem}/>
} else {
// TODO should be able to customize the item rendering from outside
return <span>{" -> " + gnItem}</span>
}
}.bind(this))}
</span>
);
}
})
The client code looks like
客户端代码看起来像
React.render(
<ImmutableListRenderer list={ImmutableList}/>,
document.getElementById('container')
);
Here is a JsFiddlethat logs the number of shouldComponentUpdate
calls after a single element of the list (size N) is updated: this does not require to call N times shouldComponentUpdate
这是一个JsFiddle,它shouldComponentUpdate
在列表的单个元素(大小 N)更新后记录调用次数:这不需要调用 N 次shouldComponentUpdate
Further implementation details are shared in this ImmutableJs github issue
此ImmutableJs github 问题中共享了进一步的实现细节
回答by mweststrate
In our product we also had issues related to the amount of code being rendered, and we started using observables (see this blog). This might partially solve your problem, as changing todo will no longer require the parent component that holds the list to be re-rendered (but adding still does).
在我们的产品中,我们也遇到了与渲染代码量相关的问题,我们开始使用 observables(请参阅此博客)。这可能会部分解决您的问题,因为更改 todo 将不再需要重新渲染包含列表的父组件(但添加仍然需要)。
It might also help you in re-rendering the list faster as your todoItem components could just return false when the props change on shouldComponentUpdate.
它还可以帮助您更快地重新呈现列表,因为当 shouldComponentUpdate 上的道具更改时,您的 todoItem 组件可能只会返回 false。
For further performance improvements when rendering the overview, I think your tree / paging idea is nice indeed. With observable arrays, each page could start to listen to array splices (using an ES7 polyfill or mobservable) in a certain range. That would introduce some administration, as these ranges might change over time, but should get you to O(log(n))
为了在渲染概览时进一步提高性能,我认为您的树/分页想法确实不错。使用 observable 数组,每个页面都可以开始侦听特定范围内的数组拼接(使用 ES7 polyfill 或 mobservable)。这会引入一些管理,因为这些范围可能会随着时间的推移而改变,但应该会让你达到 O(log(n))
So you get something like:
所以你会得到类似的东西:
var TodosContainer = React.createClass({
componentDidMount() {
this.props.todos.observe(function(change) {
if (change.type === 'splice' && change.index >= this.props.startRange && change.index < this.props.endRange)
this.forceUpdate();
});
},
renderTodo: function(todo) {
return <Todo key={todo.id} todoData={todo} x={this.props.x} y={this.props.y} .../>;
},
render: function() {
var todos = this.props.todos.slice(this.props.startRange, this.props.endRange).map(this.renderTodo)
return (
<ReactCSSTransitionGroup transitionName="transition-todo">
{todos}
</ReactCSSTransitionGroup>,
);
}
});
The central problem with large lists and react seems that you cannot just shift new DOM nodes into the dom. Othwerwise you won't need the 'pages' at all to partition the data in smaller chunks and you could just splice one new Todo item into the dom, as done with JQuery in this jsFiddle. You could still do that with react if you use a ref
for each todo item, but that would be working around the system I think as it might break the reconciliation system?
大列表和 react 的核心问题似乎是你不能仅仅将新的 DOM 节点转移到 dom 中。否则,您根本不需要“页面”来将数据分成更小的块,您只需将一个新的 Todo 项目拼接到 dom 中,就像在这个jsFiddle 中使用 JQuery 所做的那样。如果您ref
为每个待办事项使用一个,您仍然可以使用 react 来做到这一点,但我认为这将围绕系统工作,因为它可能会破坏对帐系统?
回答by Kornelius
Recently I had a performance bottleneck trying to render a table with 500+ records, the reducers were immutable and I was using reselect to memoize the complex selectors, after pulling some hair, I found the problem was solved memoizing all the selectors.
最近我在尝试渲染一个包含 500 多条记录的表时遇到了性能瓶颈,reducers 是不可变的,我使用 reselect 来记忆复杂的选择器,在拉了一些头发后,我发现问题解决了记忆所有选择器。