[英]How to optimize small updates to props of nested component in 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
我有上面的組件,Repo和RepoList。 我想更新第一個倉庫的標簽( 第14行 )。 所以我發送了一個UPDATE_TAG
動作。 在我實現shouldComponentUpdate
之前,調度大約需要shouldComponentUpdate
毫秒,這是預期的,因為我們浪費了大量時間來區分沒有改變的<Repo/>
。
添加了shouldComponentUpdate
,dispatch大約需要30ms。 在生產構建React.js之后,更新僅花費大約17ms。 這要好得多,但Chrome開發者控制台中的時間線視圖仍然表示jank幀(超過16.6ms)。
想象一下,如果我們有這樣的許多更新,或者<Repo/>
比當前更復雜,我們將無法維持60fps。
我的問題是,對於嵌套組件的道具的這種小更新,是否有更高效和規范的方式來更新內容? 我還可以使用Redux嗎?
通過用可觀察的內部減速器替換每個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中的反模式。
我按照Dan Abramov的回答中的建議, 將我的狀態和更新的連接組件 規范化
新的狀態形狀是:
{
repoIds: ['1', '2', '3', ...],
reposById: {
'1': {...},
'2': {...}
}
}
我在ReactDOM.render
周圍添加了console.time
來ReactDOM.render
初始渲染時間。
但是,性能比以前更差(初始渲染和更新)。 (來源: 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/>
上的連接都有很多開銷。
根據Dan的更新答案,我們必須返回connect
的mapStateToProps
參數,而不是返回一個函數。 你可以查看Dan的答案。 我還更新了演示 。
下面,我的電腦性能要好得多。 而且為了好玩,我還添加了我所談到的減速器方法的副作用( 來源 , 演示 )( 嚴重的是不使用它,它僅用於實驗 )。
// 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
剛剛添加了基於“每次記憶連接”的反應虛擬化示例
INITIAL: 31.878ms
DISPATCH: 4.549ms
我不確定const App = connect((state) => state)(RepoList)
來自const App = connect((state) => state)(RepoList)
。
React Redux文檔中的相應示例有一個通知 :
不要這樣做! 它會殺死任何性能優化,因為TodoApp會在每次操作后重新渲染。 最好在視圖層次結構中的幾個組件上使用更細粒度的connect(),每個組件只監聽狀態的相關片段。
我們不建議使用此模式。 相反,每個都特別連接<Repo>
因此它在mapStateToProps
讀取自己的數據。 “ 樹視圖 ”示例顯示了如何執行此操作。
如果您的狀態造型比較標准化 (現在它所有嵌套),可以單獨repoIds
從reposById
,然后只有你RepoList
如果重新渲染repoIds
變化。 這種方式更改為單個repos不會影響列表本身,只會重新呈現相應的Repo
。 這個拉取請求可能會讓您了解它是如何工作的。 “ 真實世界 ”示例顯示了如何編寫處理規范化數據的Reducer。
請注意,為了真正從規范化樹所提供的性能中獲益,您需要執行與此拉取請求完全相同的操作,並將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()
中實現它。
這種方法在我的測試中勝過以前的兩種方法:
one-connect.js: 43.272ms
repo-connect.js before changes: 61.781ms
repo-connect.js after changes: 19.954ms
最后,如果你需要顯示如此大量的數據,它無論如何都無法放入屏幕。 在這種情況下,更好的解決方案是使用虛擬化表,以便您可以渲染數千行而不會實際顯示它們的性能開銷。
通過用可觀察的內部減速器替換每個標簽,我得到了一個解決方案。
如果它有副作用,它不是Redux減速器。 它可能有用,但我建議在Redux之外放置這樣的代碼以避免混淆。 Redux onNext
必須是純函數,並且它們可能不會在主題上調用onNext
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.