如何优化React + Redux中嵌套组件的小道具更新?

示例代码: https : //github.com/d6u/example-redux-update-nested-props/blob/master/one-connect/index.js

观看现场演示: http : //d6u.github.io/example-redux-update-nested-props/one-connect.html

如何优化嵌套组件的道具的小更新?

我有上面的组件,回购和RepoList。 我想更新第一个回购的标签( 第14行 )。 所以我派出了一个UPDATE_TAG动作。 在我实现了shouldComponentUpdate之前,调度需要大约200ms,这是预料之中的,因为我们正在浪费大量时间差异<Repo/> s没有改变。

添加shouldComponentUpdate ,调度需要大约30ms。 在生产生成React.js之后,更新仅花费约17ms。 这是好得多,但在Chrome开发控制台的时间表视图仍然表明无框架(超过16.6毫秒)。

在这里输入图像说明

想象一下,如果我们有这样的更新,或者<Repo/>比现在更复杂,我们将无法保持60fps。

我的问题是,对于嵌套组件的道具的这种小的更新,是否有更高效和规范的方式来更新内容? 我还可以使用Redux吗?

我得到了一个解决scheme,用一个可观察的内部减速器replace每个tags 。 就像是

 // inside reducer when handling UPDATE_TAG action // repos[0].tags of state is already replaced with a Rx.BehaviorSubject get('repos[0].tags', state).onNext([{ id: 213, text: 'Node.js' }]); 

然后我使用https://github.com/jayphelps/react-observable-subscribe在Repo组件里订阅它们的值。 这很好。 即使有React.js的开发版本,每个调度也只需要5ms。 但是我觉得这是Redux中的反模式。

更新1

我遵循丹·阿布拉莫夫的回答中的build议, 规范了我的状态并更新了连接组件

新的状态是:

 { repoIds: ['1', '2', '3', ...], reposById: { '1': {...}, '2': {...} } } 

我添加了围绕ReactDOM.render console.time来定时初始渲染 。

但是,性能比以前更差(初始渲染和更新)。 (来源: https : //github.com/d6u/example-redux-update-nested-props/blob/master/repo-connect/index.js ,现场演示: http : //d6u.github.io/example- redux-update-nested-props / repo-connect.html )

 // With dev build INITIAL: 520.208ms DISPATCH: 40.782ms // With prod build INITIAL: 138.872ms DISPATCH: 23.054ms 

在这里输入图像说明

我想连接每个<Repo/>都有很多开销。

更新2

基于丹的更新答案,我们必须返回connectmapStateToProps参数返回一个函数。 你可以看看丹的答案。 我也更新了演示 。

下面,我的电脑上的performance要好得多。 为了好玩,我还加了减速器方法中的副作用( 源代码 , 演示 )( 严重不要使用它,只是为了实验 )。

 // in prod build (not average, very small sample) // one connect at root INITIAL: 83.789ms DISPATCH: 17.332ms // connect at every <Repo/> INITIAL: 126.557ms DISPATCH: 22.573ms // connect at every <Repo/> with memorization INITIAL: 125.115ms DISPATCH: 9.784ms // observables + side effect in reducers (don't use!) INITIAL: 163.923ms DISPATCH: 4.383ms 

更新3

刚刚添加的反应 – 虚拟化的例子基于“连接记忆”

 INITIAL: 31.878ms DISPATCH: 4.549ms 

我不知道const App = connect((state) => state)(RepoList)来自const App = connect((state) => state)(RepoList)
React Redux文档中的相应示例有一个通知 :

不要这样做! 它会杀死任何性能优化,因为TodoApp将在每次操作后重新提交。 最好在视图层次结构中的多个组件上进行更细化的连接(),每个组件只能监听状态的相关切片。

我们不build议使用这种模式。 相反,每个连接<Repo>具体,所以它在其mapStateToProps读取自己的数据。 “ 树视图 ”示例显示如何执行此操作。

如果你使状态形状更规范化 (现在它全部嵌套),你可以从reposById分离出reposById ,然后只有当repoIds改变时,你的RepoList重新渲染。 这种方式改变个人回购不会影响名单本身,只有相应的Repo将被重新呈现。 这个拉取请求可能会让你知道这个如何工作。 “ 真实世界 ”示例显示了如何编写处理归一化数据的reducer。

请注意,为了真正从标准化树提供的性能中获益,您需要像这样完成pull请求 ,并将mapStateToProps()工厂传递给connect()

 const makeMapStateToProps = (initialState, initialOwnProps) => { const { id } = initialOwnProps const mapStateToProps = (state) => { const { todos } = state const todo = todos.byId[id] return { todo } } return mapStateToProps } export default connect( makeMapStateToProps )(TodoItem) 

这是重要的原因是因为我们知道ID永远不会改变。 使用ownProps会带来性能上的损失:任何时候外部道具都会改变,内部道具必须重新计算。 然而,使用initialOwnProps不会招致这种惩罚,因为它只使用一次。

您的示例的快速版本将如下所示:

 import React from 'react'; import ReactDOM from 'react-dom'; import {createStore} from 'redux'; import {Provider, connect} from 'react-redux'; import set from 'lodash/fp/set'; import pipe from 'lodash/fp/pipe'; import groupBy from 'lodash/fp/groupBy'; import mapValues from 'lodash/fp/mapValues'; const UPDATE_TAG = 'UPDATE_TAG'; const reposById = pipe( groupBy('id'), mapValues(repos => repos[0]) )(require('json!../repos.json')); const repoIds = Object.keys(reposById); const store = createStore((state = {repoIds, reposById}, action) => { switch (action.type) { case UPDATE_TAG: return set('reposById.1.tags[0]', {id: 213, text: 'Node.js'}, state); default: return state; } }); const Repo = ({repo}) => { const [authorName, repoName] = repo.full_name.split('/'); return ( <li className="repo-item"> <div className="repo-full-name"> <span className="repo-name">{repoName}</span> <span className="repo-author-name"> / {authorName}</span> </div> <ol className="repo-tags"> {repo.tags.map((tag) => <li className="repo-tag-item" key={tag.id}>{tag.text}</li>)} </ol> <div className="repo-desc">{repo.description}</div> </li> ); } const ConnectedRepo = connect( (initialState, initialOwnProps) => (state) => ({ repo: state.reposById[initialOwnProps.repoId] }) )(Repo); const RepoList = ({repoIds}) => { return <ol className="repos">{repoIds.map((id) => <ConnectedRepo repoId={id} key={id}/>)}</ol>; }; const App = connect( (state) => ({repoIds: state.repoIds}) )(RepoList); console.time('INITIAL'); ReactDOM.render( <Provider store={store}> <App/> </Provider>, document.getElementById('app') ); console.timeEnd('INITIAL'); setTimeout(() => { console.time('DISPATCH'); store.dispatch({ type: UPDATE_TAG }); console.timeEnd('DISPATCH'); }, 1000); 

请注意,我更改ConnectedRepo connect()以使用具有initialOwnProps而非ownProps的工厂。 这可以让React Redux跳过所有的道具重新评估。

我还删除了<Repo>上不必要的shouldComponentUpdate() ,因为React Redux负责在connect()中实现它。

在我的testing中,这种方法胜过以前的两种方法:

 one-connect.js: 43.272ms repo-connect.js before changes: 61.781ms repo-connect.js after changes: 19.954ms 

最后,如果你需要显示如此大量的数据,无论如何都不能放在屏幕上。 在这种情况下,更好的解决scheme是使用虚拟化表格,这样您就可以渲染数千行而无需实际显示它们的性能开销。


我得到了一个解决scheme,用一个可观察的内部减速器replace每个标签。

如果它有副作用,它不是一个Redux减速器。 它可能工作,但我build议在Redux之外放置这样的代码以避免混淆。 Redux减速器必须是纯函数,并且不能在主题上调用onNext