如何优化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
基于丹的更新答案,我们必须返回connect
的mapStateToProps
参数返回一个函数。 你可以看看丹的答案。 我也更新了演示 。
下面,我的电脑上的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
。