React的出色表现

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

在此处输入图片说明

前提

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

  • 状态位于最高级别的Search组件中。
  • 状态描述如下:
{
    可见:布尔值,
    文件:数组,
    过滤:数组,
    请求参数,
    currentSelectedIndex:整数
}
  • files 是一个可能非常大的包含文件路径的数组(10000个条目是一个合理的数字)。
  • filtered是用户键入至少2个字符后的过滤数组。我知道它是派生数据,因此可以就将其存储在状态中进行论证,但对于
  • currentlySelectedIndex 这是过滤列表中当前选定元素的索引。

  • User types more than 2 letters into the Input component, the array is filtered and for each entry in the filtered array a Result component is rendered

  • Each Result component is displaying the full path that partially matched the query, and the partial match part of the path is highlighted. For example the DOM of a Result component, if the user had typed 'le' would be something like this :

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

  • If the user presses the up or down keys while the Input component is focused the currentlySelectedIndex changes based on the filtered array. This causes the Result component that matches the index to be marked as selected causing a re-render

PROBLEM

Initially I tested this with a small enough array of files, using the development version of React, and all worked fine.

The problem appeared when I had to deal with a files array as big as 10000 entries. Typing 2 letters in the Input would generate a big list and when I pressed the up and down keys to navigate it it would be very laggy.

At first I did not have a defined component for the Result elements and I was merely making the list on the fly, on each render of the Search component, as such:

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));

As you can tell, every time the currentlySelectedIndex changed, it would cause a re-render and the list would be re-created each time. I thought that since I had set a key value on each li element React would avoid re-rendering every other li element that did not have its className change, but apparently it wasn't so.

I ended up defining a class for the Result elements, where it explicitly checks whether each Result element should re-render based on whether it was previously selected and based on the current user input :

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>
        );
    }
});

And the list is now created as such:

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));
}

This made performance slightly better, but it's still not good enough. Thing is when I tested on the production version of React things worked buttery smooth, no lag at all.

BOTTOMLINE

Is such a noticeable discrepancy between development and production versions of React normal?

Am I understanding/doing something wrong when I think about how React manages the list?

UPDATE 14-11-2016

I have found this presentation of Michael Jackson, where he tackles an issue very similar to this one: https://youtu.be/7S8v8jfLb1Q?t=26m2s

The solution is very similar to the one proposed by AskarovBeknar's answer, below

UPDATE 14-4-2018

Since this is apparently a popular question and things have progressed since the original question was asked, while I do encourage you to watch the video linked above, in order to get a grasp of a virtual layout, I also encourage you to use the React Virtualized library if you do not want to re-invent the wheel.

逆天古一Mandy2020/03/16 09:58:55

在加载到React组件之前尝试进行过滤,并且仅在组件中显示合理数量的项目,然后根据需要加载更多内容。没有人可以一次查看很多项目。

我认为您不是,但不要使用索引作为键

要找出开发版本和生产版本不同的真正原因,可以尝试使用profiling代码。

加载页面,开始记录,进行更改,停止记录,然后检查时间。有关在Chrome中进行性能分析的说明,请参见此处

APro2020/03/16 09:58:55

对于任何为这个问题苦苦挣扎的人,我已经编写了一个组件react-big-list来处理多达一百万条记录的列表。

最重要的是,它还提供了一些新颖的功能,例如:

  • 排序
  • 快取
  • 自定义过滤
  • ...

我们在很多应用程序中都在生产中使用它,并且效果很好。

宝儿老丝2020/03/16 09:58:55

我对一个非常类似的问题的经验是,如果DOM中的组件一次超过100-200个左右,那么反应确实会受到影响。即使您非常谨慎(通过设置所有键和/或实现一种shouldComponentUpdate方法)仅更改一个或两个重新渲染的组件,您仍然会遭受重创。

目前,反应最慢的部分是比较虚拟DOM和真实DOM之间的差异。如果您有成千上万个组件,但只更新了几个,则没关系,React在DOM之间仍然有巨大的差异。

现在,当我编写页面时,我会尝试设计它们以最大程度地减少组件数量,呈现大量组件时执行此操作的一种方法是...好吧...而不呈现大量组件。

我的意思是:仅渲染当前可以看到的组件,向下滚动时渲染更多,用户不太可能以任何方式向下滚动成千上万个组件....我希望。

一个很棒的库是:

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

这里有一个很棒的方法:

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

恐怕它不会删除位于页面顶部的组件,因此,如果滚动足够长的时间,性能问题将会再次出现。

我知道提供链接作为答案不是一个好习惯,但是他们提供的示例将比我在此处更好地解释如何使用该库。希望我已经解释了为什么大名单不好,但也可以解决。

Mandy仲羽2020/03/16 09:58:55

与该问题的许多其他答案一样,主要问题在于以下事实:在执行DOM过滤和处理关键事件的同时渲染DOM中的这么多元素会很慢。

对于导致问题的React,您没有做任何天生的错误,但是与性能相关的许多问题一样,UI也可能引起很大的责任。

如果您的UI在设计时没有考虑效率,那么甚至像React这样被设计为高性能的工具也将遭受损失。

正如@Koen所提到的,过滤结果集是一个不错的开始

我已经对该想法进行了一些尝试,并创建了一个示例应用程序来说明如何开始解决此类问题。

这绝不是production ready代码,但它确实充分说明了该概念,可以对其进行修改以使其更加健壮,可以随时查看代码-我希望至少它能给您一些想法...;)

反应一个大列表的例子

在此处输入图片说明

L村村小宇宙2020/03/16 09:58:55
  1. 在开发版本中使用React检查每个组件的原型,以简化开发过程,而在生产中则省略。

  2. 过滤字符串列表对于每个键入都是非常昂贵的操作。由于JavaScript的单线程特性,可能会导致性能问题。解决方案可能是使用反跳方法来延迟执行过滤器功能,直到延迟结束。

  3. 另一个问题可能是庞大的清单本身。您可以创建虚拟布局,并仅替换数据即可重复使用创建的项目。基本上,您创建具有固定高度的可滚动容器组件,在其中放置列表容器。列表容器的高度应根据可见列表的长度手动设置(itemHeight * numberOfItems),以使滚动条起作用。然后创建一些项目组件,以便它们将填充可滚动容器的高度,并可能添加额外的一两个模拟连续列表效果。使它们处于绝对位置,并在滚动时只需移动其位置即可模仿连续列表(我想您会发现如何实现它:)

  4. 还有一件事是写DOM也是昂贵的操作,尤其是如果您做错了。您可以使用画布显示列表,并在滚动时创建流畅的体验。检出react-canvas组件。我听说他们已经在Lists上做了一些工作。

GOLEY前端2020/03/16 09:58:55

看看React Virtualized Select,它旨在解决这个问题,并且以我的经验令人印象深刻。根据说明:

HOC使用react-virtualized和react-select在下拉菜单中显示大量选项

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

樱十三2020/03/16 09:58:55

就像我在评论中提到的那样,我怀疑用户一次需要浏览器中的所有10000个结果。

如果您翻阅结果并始终只显示10个结果的列表,该怎么办。

使用这种技术创建了一个示例,而没有使用Redux之类的任何其他库。目前仅使用键盘导航,但可以轻松扩展以用于滚动。

该示例包含3个组件,容器应用程序,搜索组件和列表组件。几乎所有逻辑都已移至容器组件。

要旨在于跟踪startselected结果,并在键盘交互时转移结果。

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})
  }
},

在简单地通过过滤器传递所有文件的同时:

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

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

和切片成果的基础上start,并limitrender方法:

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>
  )
}

小提琴包含完整的工作示例:https//jsfiddle.net/koenpunt/hm1xnpqk/