React 的大列表性能

IT技术 javascript list reactjs
2021-04-21 10:36:12

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

在此处输入图片说明

前提

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

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

  • 用户在Input组件中输入2 个以上的字母,数组被过滤,对于过滤数组中的每个条目,一个Result组件被呈现

  • 每个Result组件都显示与查询部分匹配的完整路径,并突出显示路径的部分匹配部分。例如,一个 Result 组件的 DOM,如果用户输入了 'le' 将会是这样的:

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

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

问题

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

当我不得不处理一个files有 10000 个条目数组时,问题就出现了在 Input 中输入 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 的生产版本上进行测试时,一切都非常顺利,完全没有延迟。

底线

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

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

更新 14-11-2016

我找到了迈克尔杰克逊的这个演讲,他在那里解决了一个与这个问题非常相似的问题:https : //youtu.be/7S8v8jfLb1Q?t=26m2s

该解决方案与下面AskarovBeknar 的回答提出的解决方案非常相似

更新 14-4-2018

由于这显然是一个受欢迎的问题,并且自从提出原始问题以来事情已经取得了进展,虽然我鼓励您观看上面链接的视频,为了掌握虚拟布局,我也鼓励您使用React Virtualized图书馆,如果你不想重新发明轮子。

6个回答

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

对于导致问题的 React,您并没有做任何本质上的错误,但与许多与性能相关的问题一样,UI 也可以承担很大一部分责任。

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

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

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

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

react大列表示例

在此处输入图片说明

@stackjlei 我认为他的意思是将 127.0.0.1 映射到/etc/hosts 中的localhost:3001
2021-05-31 10:36:12
我真的为不得不选择一个答案而感到难过,他们似乎都付出了努力,但我目前正在度假,没有电脑,无法真正以应有的关注来检查它们。我之所以选择这个,是因为它足够短而且重点突出,即使在通过手机阅读时也能理解。我知道的蹩脚原因。
2021-06-12 10:36:12
编辑主机文件是什么意思127.0.0.1 * http://localhost:3001
2021-06-19 10:36:12

我对一个非常相似的问题的经验是,如果 DOM 中同时有超过 100-200 个左右的组件,react 真的会受到影响。即使您非常小心(通过设置所有键和/或实现一个shouldComponentUpdate方法)只在重新渲染时更改一两个组件,您仍然会处于受伤的世界中。

目前react比较慢的部分是比较虚拟DOM和真实DOM的区别。如果你有数千个组件但只更新几个,没关系,react 在 DOM 之间仍然有一个巨大的差异操作要做。

当我现在编写页面时,我尝试设计它们以最小化组件的数量,在呈现大量组件列表时执行此操作的一种方法是......好吧......不呈现大量组件列表。

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

一个伟大的图书馆这样做是:

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

这里有一个很好的操作方法:

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

恐怕它不会删除页面顶部的组件,因此如果您滚动足够长的时间,您的性能问题将开始重新出现。

我知道提供链接作为答案并不是一个好习惯,但是他们提供的示例将比我在这里更好地解释如何使用这个库。希望我已经解释了为什么大列表不好,但也是一种解决方法。

更新:不维护此答案中的包。npmjs.com/package/react-infinite-scroller上设置了一个 fork
2021-06-06 10:36:12
除非无限滚动实现使用类似 Android 的 RecyclerView 方法,否则性能问题迟早会重新出现。Afaik,React 中没有这种版本的无限滚动。
2021-06-19 10:36:12

首先,React 的开发和生产版本之间的差异是巨大的,因为在生产中存在许多绕过的健全性检查(例如 prop 类型验证)。

然后,我认为您应该重新考虑使用 Redux,因为它对您需要的东西(或任何类型的通量实现)非常有帮助。您绝对应该看看这个演示文稿:Big List High Performance React & Redux

但是在深入研究 redux 之前,您需要通过将组件拆分为更小的组件来对您的 React 代码进行一些调整,因为这shouldComponentUpdate将完全绕过子组件的渲染,因此这是一个巨大的收获

当您有更细粒度的组件时,您可以使用 redux 和 react-redux 处理状态以更好地组织数据流。

我最近遇到了类似的问题,当我需要渲染一千行并能够通过编辑其内容来修改每一行时。这个迷你应用程序显示了一个音乐会列表,其中包含潜在的重复音乐会,如果我想通过选中复选框将潜在的重复标记为原始音乐会(而不是重复),我需要为每个潜在的重复进行选择,并在必要时编辑演唱会名称。如果我对特定的潜在重复项不做任何处理,它将被视为重复项并将被删除。

这是它的样子:

在此处输入图片说明

基本上有 4 个电源组件(这里只有一行,但这是为了示例):

在此处输入图片说明

这是使用reduxreact-reduximmutablereselectrecompose的完整代码(工作 CodePen : Huge List with React & Redux

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

在处理庞大的数据集时通过做这个迷你应用程序学到的经验教训

  • React 组件在保持较小时效果最佳
  • 重新选择对于避免重新计算并保持相同的引用对象(使用 immutable.js 时)给定相同的参数非常有用。
  • connect为最接近他们需要的数据的组件创建ed 组件,以避免组件只传递他们不使用的props
  • 当您只需要给出的初始props时,使用结构函数来创建 mapDispatchToPropsownProps是必要的,以避免无用的重新渲染
  • React 和 redux 绝对结合在一起!
我不认为添加对 redux 的依赖对于解决 OP 的问题是必要的,更多的调度操作来过滤他的结果集只会使问题复杂化,调度并不像你想象的那么便宜,用本地组件处理这种特殊情况状态是最有效的方法
2021-06-01 10:36:12

就像我在评论中提到的那样,我怀疑用户是否需要一次在浏览器中获得所有这 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/

在加载到 React 组件之前尝试过滤器,只显示组件中合理数量的项目,并根据需要加载更多。没有人可以一次查看这么多项目。

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

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

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