小能豆

使用 React 实现大列表性能

javascript

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

lTcZm.png

前提

以下是对其工作原理的描述:

  • 状态驻留在最高级别的组件即Search组件中。
  • 该状态描述如下:
{
    可见:布尔值,
    文件:数组,
    过滤:数组,
    请求参数,
    当前选定索引: 整数
}
  • files是一个可能非常大的包含文件路径的数组(10000 个条目是一个合理的数字)。

  • filtered是用户输入至少 2 个字符后过滤后的数组。我知道它是派生数据,因此可以将其存储在状态中,但这是必要的

  • currentlySelectedIndex这是从过滤列表中当前选定的元素的索引。

  • 用户在组件中输入超过 2 个字母Input,数组会被过滤,并且过滤后的数组中的每个条目Result都会被渲染成一个组件

  • 每个Result组件都显示与查询部分匹配的完整路径,并且路径的部分匹配部分会突出显示。例如,如果用户输入了“le”,则 Result 组件的 DOM 将如下所示:

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

  • 如果用户在组件Input聚焦时按下向上或向下键currentlySelectedIndex,则根据filtered数组进行更改。这会导致Result与索引匹配的组件被标记为已选中,从而导致重新渲染

问题

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

当我必须处理files多达 10000 个条目的数组时,问题就出现了。在输入中输入 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更改都会导致重新渲染,并且每次都会重新创建列表。我以为,既然我已经key为每个li元素设置了一个值,React 就会避免重新渲染li没有更改的其他每个元素className,但显然事实并非如此。

我最终为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 的生产版本上测试时,一切都运行顺畅,没有任何延迟。


阅读 63

收藏
2024-06-12

共1个答案

小能豆

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

就 React 而言,你并没有做任何本质上错误的事情,而这恰恰导致了这个问题,但与许多与性能相关的问题一样,UI 也要承担很大一部分责任。

如果您的 UI 设计没有考虑效率,即使是像 React 这样专为高性能而设计的工具也会受到影响。

正如@Koen 提到的,过滤结果集是一个很好的开始

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

这绝不是production ready代码,但它确实充分说明了概念,并且可以修改为更加健壮,请随意查看代码 - 我希望至少它能给你一些想法......;)

react-large-list-示例

在此处输入图片描述

llyiL.gif

2024-06-12