簡體   English   中英

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

[英]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中的反模式。

更新1

我按照Dan Abramov的回答中的建議, 將我的狀態更新的連接組件 規范化

新的狀態形狀是:

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

我在ReactDOM.render周圍添加了console.timeReactDOM.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/>上的連接都有很多開銷。

更新2

根據Dan的更新答案,我們必須返回connectmapStateToProps參數,而不是返回一個函數。 你可以查看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

更新3

剛剛添加了基於“每次記憶連接”的反應虛擬化示例

INITIAL: 31.878ms
DISPATCH: 4.549ms

我不確定const App = connect((state) => state)(RepoList)來自const App = connect((state) => state)(RepoList)
React Redux文檔中相應示例有一個通知

不要這樣做! 它會殺死任何性能優化,因為TodoApp會在每次操作后重新渲染。 最好在視圖層次結構中的幾個組件上使用更細粒度的connect(),每個組件只監聽狀態的相關片段。

我們不建議使用此模式。 相反,每個都特別連接<Repo>因此它在mapStateToProps讀取自己的數據。 樹視圖 ”示例顯示了如何執行此操作。

如果您的狀態造型比較標准化 (現在它所有嵌套),可以單獨repoIdsreposById ,然后只有你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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM