React 的大名单性能

Posted

技术标签:

【中文标题】React 的大名单性能【英文标题】:Big list performance with React 【发布时间】:2016-10-28 06:09:22 【问题描述】:

我正在使用 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 的开发版本,一切正常。

当我不得不处理一个包含 10000 个条目的 files 数组时,问题就出现了。在 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 更改时,都会导致重新渲染,并且每次都会重新创建列表。我认为既然我在每个 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 的开发和生产版本之间存在如此明显的差异是否正常?

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

2016 年 14 月 11 日更新

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

该解决方案与 AskarovBeknar 的answer 提出的解决方案非常相似,如下

2018 年 4 月 14 日更新

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

【问题讨论】:

React 的开发/生产版本是什么意思? @Dibesjr facebook.github.io/react/… 啊,我明白了,谢谢。因此,要回答您的一个问题,它表示版本之间的优化存在差异。在大列表中要注意的一件事是在渲染中创建函数。当您进入庞大的列表时,它将对性能产生影响。我会尝试看看使用他们的性能工具生成该列表需要多长时间facebook.github.io/react/docs/perf.html 我认为您应该重新考虑使用 Redux,因为这正是您需要的(或任何类型的通量实现)。你应该明确地看看这个演示文稿:Big List High Performance React & Redux 我怀疑用户滚动浏览 10000 个结果有什么好处。那么,如果您只呈现前 100 个左右的结果,并根据查询更新这些结果会怎样。 【参考方案1】:

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

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

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

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

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

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

react-large-list-example

【讨论】:

我真的为不得不选择一个答案而感到难过,他们似乎都付出了努力,但我目前正在度假,没有电脑,无法真正以他们应得的注意力来检查他们.我之所以选择这个,是因为它足够短且中肯,即使通过手机阅读也能理解。我知道的蹩脚理由。 编辑主机文件127.0.0.1 * http://localhost:3001是什么意思? @stackjlei 我认为他的意思是在 /etc/hosts 中将 127.0.0.1 映射到 localhost:3001【参考方案2】:

我对一个非常相似的问题的经验是,如果 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 除非无限滚动实现使用类似 android 的 RecyclerView 方法,否则性能问题迟早会再次出现。 Afaik,React 中没有这样的无限滚动版本。【参考方案3】:

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

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

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

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

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

这是它的样子:

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

这是使用redux、react-redux、immutable、reselect和recompose的完整代码(工作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 时)非常有用。 为最接近所需数据的组件创建connected 组件,以避免组件只传递他们不使用的道具 当您只需要ownProps 中给出的初始道具时,使用fabric 函数创建mapDispatchToProps 是必要的,以避免无用的重新渲染 React 和 redux 绝对结合在一起!

【讨论】:

我不认为添加对 redux 的依赖对于解决 OP 的问题是必要的,更多用于过滤他的结果集的调度操作只会使问题更加复杂,调度并不像你想象的那么便宜,使用本地组件状态处理这种特殊情况是最有效的方法【参考方案4】:

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

如果您对结果进行分页,总是只显示 10 个结果的列表。

我已经created an example 使用了这种技术,而没有使用任何其他库,例如 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
  );
,

并在render 方法中根据startlimit 对结果进行切片:

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/

【讨论】:

【参考方案5】:

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

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

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

加载您的页面,开始录制,执行更改,停止录制,然后查看时间。见here for instructions for performance profiling in Chrome。

【讨论】:

【参考方案6】:

    React 在开发版本中检查每个组件的 proptypes 以简化开发过程,而在生产中则省略。

    过滤字符串列表对于每个 keyup 来说都是非常昂贵的操作。由于 javascript 的单线程特性,它可能会导致性能问题。 解决方案可能是使用 debounce 方法来延迟执行过滤器功能,直到延迟到期。

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

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

【讨论】:

关于React in development的任何信息?为什么要检查每个组件的原型?【参考方案7】:

查看 React Virtualized Select,它旨在解决这个问题,并且在我的经验中表现出色。来自描述:

使用 react-virtualized 和 react-select 在下拉列表中显示大量选项的 HOC

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

【讨论】:

【参考方案8】:

对于任何遇到此问题的人,我编写了一个组件 react-big-list,它可以处理多达 100 万条记录的列表。

除此之外,它还带有一些花哨的额外功能,例如:

排序 缓存 自定义过滤 ...

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

【讨论】:

【参考方案9】:

React 有推荐 react-window 库:https://www.npmjs.com/package/react-window

react-vitualized 好。你可以试试

【讨论】:

以上是关于React 的大名单性能的主要内容,如果未能解决你的问题,请参考以下文章

React native + redux-persist:如何忽略键(黑名单)?

在 React-Native 项目中开玩笑。如何将重复模块列入黑名单或删除?

Android P 性能优化:创建APP进程白名单,杀死白名单之外的进程

Nginx一网打尽:动静分离压缩缓存黑白名单跨域高可用性能优化......

Nginx一网打尽:动静分离压缩缓存黑白名单跨域高可用性能优化......

风控核心子域——名单服务构建及挑战