React的出色表现


86

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

在此处输入图片说明

前提

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

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

  • 用户在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的生产版本上进行测试时,一切顺利进行,完全没有滞后。

底线

React开发版本和生产版本之间的这种明显差异正常吗?

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

更新14-11-2016

我发现了迈克尔·杰克逊(Michael Jackson)的演示文稿,他在其中解决了一个与此非常类似的问题:https : //youtu.be/7S8v8jfLb1Q?t=26m2s

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

更新14-4-2018

由于这显然是一个受欢迎的问题,并且自提出原始问题以来情况已经有所改善,尽管我确实鼓励您观看上面链接的视频,以便掌握虚拟布局,但我也鼓励您使用React Virtualized库,如果您不想重新发明轮子。


您所说的反应的开发/生产版本是什么意思?
Dibesjr,2016年


我明白了,谢谢。因此,要回答您的一个问题,它说版本之间的优化存在差异。在大型列表中要注意的一件事是在渲染中创建函数。当您进入庞大的列表时,它将对性能产生影响。我会尝试,看看需要多长时间使用他们的PERF工具来生成列表facebook.github.io/react/docs/perf.html
Dibesjr

2
我认为您应该重新考虑使用Redux,因为这正是您在这里(或任何一种流量实现)所需要的。您应该明确看一下这个演示文稿:大列表高性能React&Redux
Pierre Criulanscy

2
我怀疑用户是否可以滚动浏览10000个结果。那么,如果仅渲染前100个结果,然后根据查询更新这些结果,该怎么办?
科恩

Answers:


18

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

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

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

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

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

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

反应一个大列表的例子

在此处输入图片说明


1
我只选择一个答案确实感到很遗憾,它们似乎都花了很多力气,但是我目前正在度假,没有电脑,无法真正得到应有的重视。我之所以选择它,是因为它足够简短,而且要点很明显,即使从电话阅读时也能理解。我知道脚的原因。
Dimitris Karagiannis

编辑主机文件是什么意思127.0.0.1 * http://localhost:3001
stackjlei

@stackjlei我认为他的意思是将127.0.0.1映射到/ etc / hosts中的localhost:3001
Maverick,

16

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

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

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

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

一个很棒的库是:

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

这里有一个很棒的方法:

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

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

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


2
更新:此答案中的软件包未维护。在npmjs.com/package/react-infinite-scroller
Ali Al

11

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

然后,我认为您应该重新考虑使用Redux,因为这对于您所需要的东西(或任何一种通量实现)将非常有帮助。您绝对应该看一下这个演示:大列表高性能React&Redux

但是在进入redux之前,您需要通过将组件拆分为较小的组件来对React代码进行一些调整,因为shouldComponentUpdate它将完全绕过子代的呈现,因此这是一个巨大的收益

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

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

这是它的样子:

在此处输入图片说明

基本上有4个电源组件(这里仅一行,但出于示例目的):

在此处输入图片说明

这是使用reduxreact-reduximmutablereselectrecompose的完整代码(工作CodePen:带有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组件,该组件最接近其需要的数据,以避免组件仅传递不使用的道具
  • 当只需要给定初始道具时,必须使用构造函数来创建mapDispatchToProps,ownProps以避免不必要的重新渲染
  • React和Redux绝对在一起!

2
我认为没有必要向redux添加依赖项来解决OP的问题,更多用于过滤其结果集的调度操作只会使问题复杂化,调度并不像您想象的那样便宜,使用本地组件来处理这种特殊情况状态是最有效的方法
deowk '16

4
  1. 在开发版本中使用React检查每个组件的原型,以简化开发过程,而在生产中则省略。

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

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

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


有关的任何信息React in development?为什么要检查每种成分的原型?
Liuuil


4

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


3

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

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

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

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


2

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

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

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

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


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.