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 时)非常有用。 为最接近所需数据的组件创建connect
ed 组件,以避免组件只传递他们不使用的道具
当您只需要ownProps
中给出的初始道具时,使用fabric 函数创建mapDispatchToProps 是必要的,以避免无用的重新渲染
React 和 redux 绝对结合在一起!
【讨论】:
我不认为添加对 redux 的依赖对于解决 OP 的问题是必要的,更多用于过滤他的结果集的调度操作只会使问题更加复杂,调度并不像你想象的那么便宜,使用本地组件状态处理这种特殊情况是最有效的方法【参考方案4】:就像我在my comment 中提到的那样,我怀疑用户是否需要一次浏览器中的所有这 10000 个结果。
如果您对结果进行分页,总是只显示 10 个结果的列表。
我已经created an example 使用了这种技术,而没有使用任何其他库,例如 Redux。 目前仅支持键盘导航,但也可以轻松扩展为滚动。
示例存在 3 个组件,容器应用程序、搜索组件和列表组件。 几乎所有的逻辑都移到了容器组件中。
要点在于跟踪start
和selected
结果,并在键盘交互上转移它们。
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
方法中根据start
和limit
对结果进行切片:
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一网打尽:动静分离压缩缓存黑白名单跨域高可用性能优化......