web-dev-qa-db-fra.com

Comment optimiser les petites mises à jour des accessoires du composant imbriqué dans React + Redux?

Exemple de code: https://github.com/d6u/example-redux-update-nested-props/blob/master/one-connect/index.js

Voir la démo en direct: http://d6u.github.io/example-redux-update-nested-props/one-connect.html

Comment optimiser les petites mises à jour des accessoires du composant imbriqué?

J'ai ci-dessus des composants, Repo et RepoList. Je veux mettre à jour la balise du premier dépôt ( ligne 14 ). J'ai donc envoyé un UPDATE_TAG action. Avant d'implémenter shouldComponentUpdate, la répartition prend environ 200 ms, ce qui est attendu car nous perdons beaucoup de temps différant <Repo/>s qui n'ont pas changé.

Après avoir ajouté shouldComponentUpdate, l'expédition prend environ 30 ms. Après la production de React.js, les mises à jour ne coûtent qu'environ 17 ms. C'est beaucoup mieux, mais la vue chronologique dans Chrome indique toujours le cadre jank (plus de 16,6 ms).

enter image description here

Imaginez si nous avons beaucoup de mises à jour comme celle-ci, ou <Repo/> est plus compliqué que l'actuel, nous ne pourrons pas maintenir 60fps.

Ma question est, pour ces petites mises à jour des accessoires d'un composant imbriqué, existe-t-il un moyen plus efficace et canonique de mettre à jour le contenu? Puis-je toujours utiliser Redux?

J'ai obtenu une solution en remplaçant chaque tags par un réducteur intérieur observable. Quelque chose comme

// 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'
}]);

Ensuite, je souscris à leurs valeurs dans le composant Repo en utilisant https://github.com/jayphelps/react-observable-subscribe . Cela a très bien fonctionné. Chaque expédition ne coûte que 5 ms, même avec la version de développement de React.js. Mais j'ai l'impression que c'est un anti-pattern dans Redux.

Mise à jour 1

J'ai suivi la recommandation dans la réponse de Dan Abramov et normalisé mon état et composants de connexion mis à jour

La nouvelle forme d'état est:

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

J'ai ajouté console.time environ ReactDOM.render to time le rendu initial .

Cependant, les performances sont pires qu'auparavant (à la fois le rendu initial et la mise à jour). (Source: https://github.com/d6u/example-redux-update-nested-props/blob/master/repo-connect/index.js , démo en direct: 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

Je pense que je me connecte à chaque <Repo/> a beaucoup de frais généraux.

Update 2

Sur la base de la réponse mise à jour de Dan, nous devons renvoyer les arguments connectmapStateToProps retournent une fonction à la place. Vous pouvez vérifier la réponse de Dan. J'ai également mis à jour les démos .

Ci-dessous, les performances sont bien meilleures sur mon ordinateur. Et juste pour le plaisir, j'ai également ajouté l'effet secondaire dans l'approche réducteur dont j'ai parlé ( source , demo ) ( sérieusement ne pas utiliser c'est pour l'expérimentation uniquement ).

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

Mise à jour 3

Juste ajouté exemple de réactivation virtualisée basé sur "se connecter à tout avec mémorisation"

INITIAL: 31.878ms
DISPATCH: 4.549ms
42
Daiwei

Je ne sais pas d'où vient const App = connect((state) => state)(RepoList).
Le exemple correspondant dans React Redux docs a un avis :

Ne fais pas ça! Il tue toutes les optimisations de performances car TodoApp se rendra à nouveau après chaque action. Il est préférable d'avoir une connexion plus granulaire () sur plusieurs composants de votre hiérarchie de vues qui n'écoutent chacun qu'une tranche pertinente de l'état.

Nous ne suggérons pas d'utiliser ce modèle. Au lieu de cela, chaque connexion <Repo> Spécifiquement pour lire ses propres données dans son mapStateToProps. L'exemple " arborescence " montre comment procéder.

Si vous rendez la forme d'état plus normalisée (en ce moment, tout est imbriqué), vous pouvez séparer repoIds de reposById, puis avoir seulement votre RepoList restitue si repoIds change. De cette façon, les modifications apportées aux référentiels individuels n'affecteront pas la liste elle-même, et seul le Repo correspondant sera rendu à nouveau. Cette demande de tirage pourrait vous donner une idée de la façon dont cela pourrait fonctionner. L'exemple " monde réel " montre comment vous pouvez écrire des réducteurs qui traitent des données normalisées.

Notez que pour vraiment profiter des performances offertes par la normalisation de l'arbre, vous devez faire exactement comme cette demande de tirage le fait et passer une usine 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)

La raison pour laquelle cela est important est que nous savons que les identifiants ne changent jamais. L'utilisation de ownProps entraîne une baisse des performances: les accessoires internes doivent être recalculés chaque fois que les accessoires externes changent. Cependant, l'utilisation de initialOwnProps n'entraîne pas cette pénalité car elle n'est utilisée qu'une seule fois.

Une version rapide de votre exemple ressemblerait à ceci:

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);

Notez que j'ai changé connect() dans ConnectedRepo pour utiliser une fabrique avec initialOwnProps plutôt que ownProps. Cela permet à React Redux d'ignorer toutes les réévaluations d'accessoires.

J'ai également supprimé le shouldComponentUpdate() inutile sur le <Repo> Parce que React Redux prend soin de l'implémenter dans connect().

Cette approche bat les deux approches précédentes dans mes tests:

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

Enfin, si vous avez besoin d'afficher une telle quantité de données, elles ne peuvent de toute façon pas tenir à l'écran. Dans ce cas, une meilleure solution consiste à utiliser un table virtualisée afin que vous puissiez afficher des milliers de lignes sans que les performances ne soient réellement affichées.


J'ai obtenu une solution en remplaçant chaque étiquette par un réducteur intérieur observable.

S'il a des effets secondaires, ce n'est pas un réducteur Redux. Cela peut fonctionner, mais je suggère de mettre du code comme celui-ci en dehors de Redux pour éviter toute confusion. Les réducteurs Redux doivent être des fonctions pures et ils ne peuvent pas appeler onNext sur des sujets.

53
Dan Abramov