web-dev-qa-db-ja.com

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があります。最初のレポのタグを更新したい( Line 14 )。そこで、UPDATE_TAGアクション。 shouldComponentUpdateを実装する前に、ディスパッチには約200ミリ秒かかります。これは、<Repo/>sは変更されていません。

shouldComponentUpdateを追加した後、ディスパッチには約30ミリ秒かかります。実稼働ビルドReact.jsの後、更新のコストは約17ミリ秒です。これははるかに優れていますが、Chrome devコンソールのタイムラインビューはまだジャンクフレーム(16.6msより長い)を示しています。

enter image description here

このような多くの更新がある場合、または<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の開発ビルドでも、すべてのディスパッチの費用はわずか5ミリ秒です。しかし、これはReduxのアンチパターンのように感じます。

アップデート1

私はダン・アブラモフの回答の推奨事項に従い、 私の状態を正規化 および 更新された接続コンポーネント

新しい状態の形状は次のとおりです。

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

追加した console.time周りReactDOM.render to 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

enter image description here

<Repo/>には多くのオーバーヘッドがあります。

更新2

Danの更新された回答に基づいて、代わりにconnectmapStateToProps引数を返す必要があります。ダンの答えをご覧ください。 デモ も更新しました。

以下では、私のコンピューターのパフォーマンスがはるかに優れています。そして、単に楽しみのために、私が話したレデューサーアプローチに副作用も追加しました( sourcedemo )(それは、実験専用です)。

// 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

react-virtualized example を追加しました。

INITIAL: 31.878ms
DISPATCH: 4.549ms
42
Daiwei

const App = connect((state) => state)(RepoList)がどこから来たのかわかりません。
React Redux docsの対応する例には通知があります

これをしないでください! TodoAppはすべてのアクションの後に再レンダリングするため、パフォーマンスの最適化はすべて終了します。ビュー階層内のいくつかのコンポーネントでより詳細なconnect()を使用し、各コンポーネントが状態の関連するスライスのみをリッスンすることをお勧めします。

このパターンを使用することはお勧めしません。むしろ、それぞれが<Repo>に接続するため、mapStateToProps内の独自のデータを読み取ります。 「 tree-view 」の例は、その方法を示しています。

状態の形状をさらに 正規化 (現在はすべてネストされている)にすると、repoIdsreposByIdから分離し、RepoListのみを再作成できます。 repoIdsが変更された場合にレンダリングします。この方法で個々のリポジトリに変更してもリスト自体には影響せず、対応するRepoのみが再レンダリングされます。 このプルリクエスト は、それがどのように機能するかを示しているかもしれません。 「 real-world 」の例は、正規化されたデータを処理するレデューサーの作成方法を示しています。

ツリーを正規化することで提供されるパフォーマンスを実際に活用するには、 this pull request のように実行し、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を使用しても1回しか使用されないため、このペナルティは発生しません。

サンプルの高速バージョンは次のようになります。

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ではなくinitialOwnPropsでファクトリを使用するようにownPropsconnect()を変更したことに注意してください。これにより、React Reduxはすべてのプロップの再評価をスキップします。

React ReduxがshouldComponentUpdate()での実装を処理するため、<Repo>上の不要なconnect()も削除しました。

このアプローチは、私のテストで以前の両方のアプローチを打ち負かしています:

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

最後に、このような大量のデータを表示する必要がある場合は、とにかく画面に収まりません。この場合、 仮想化テーブル を使用して、実際に表示するパフォーマンスのオーバーヘッドなしに数千行をレンダリングできるようにすることをお勧めします。


すべてのタグを観察可能な内部レデューサーに置き換えることで解決策を得ました。

副作用がある場合は、Reduxレデューサーではありません。うまくいくかもしれませんが、混乱を避けるためにこのようなコードをReduxの外部に置くことをお勧めします。 Reduxレデューサーは純粋な関数である必要があり、サブジェクトでonNextを呼び出すことはできません。

53
Dan Abramov