44 votes

Comment optimiser les petites mises à jour des props de composants imbriqués 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 props d'un composant imbriqué ?

J'ai les composants ci-dessus, Repo et RepoList. Je veux mettre à jour la balise du premier repo ( Ligne 14 ). J'ai donc envoyé un UPDATE_TAG action. Avant de mettre en place shouldComponentUpdate la répartition prend environ 200 ms, ce qui est normal puisque nous perdons beaucoup de temps à différencier les données. <Repo/> qui n'ont pas changé.

Après avoir ajouté shouldComponentUpdate L'envoi prend environ 30 ms. Après la mise en production de React.js, les mises à jour ne coûtent que 17 ms environ. C'est beaucoup mieux, mais la vue de la ligne de temps dans la console de développement de Chrome indique toujours une trame jank (plus longue que 16,6 ms).

enter image description here

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

Ma question est la suivante : pour de si petites mises à jour des props d'un composant imbriqué, existe-t-il un moyen plus efficace et canonique de mettre à jour le contenu ? Puis-je encore utiliser Redux ?

J'ai trouvé une solution en remplaçant chaque tags avec un observable dans le réducteur. 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'
}]);

Je m'abonne ensuite à leurs valeurs dans le composant Repo à l'aide des éléments suivants https://github.com/jayphelps/react-observable-subscribe . Cela a bien fonctionné. Chaque envoi 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-modèle dans Redux.

Mise à jour 1

J'ai suivi la recommandation de la réponse de Dan Abramov et j'ai normalisé mon état et composants de connexion actualisés

La nouvelle forme de l'État est :

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

J'ai ajouté console.time autour de ReactDOM.render à l'heure le rendu initial .

Toutefois, les performances sont moins bonnes qu'auparavant (tant pour le rendu initial que pour 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 se connecter sur chaque <Repo/> a beaucoup de frais généraux.

Mise à jour 2

En se basant sur la réponse actualisée de Dan, nous devons retourner connect 's mapStateToProps renvoient une fonction à la place. Vous pouvez consulter 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 aussi ajouté l'effet secondaire dans l'approche du réducteur dont j'ai parlé ( source , Démonstration ) ( sérieusement, ne l'utilisez pas, c'est seulement pour l'expérience. ).

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

Je viens d'ajouter exemple react-virtualisé basé sur "connecter à tout avec la mémorisation"

INITIAL: 31.878ms
DISPATCH: 4.549ms

1 votes

J'ai modifié ma réponse.

0 votes

Les liens des exemples sont morts :/

59voto

Dan Points 16670

Je ne suis pas sûr de savoir où const App = connect((state) => state)(RepoList) vient de.
Le site L'exemple correspondant dans la documentation de React Redux comporte un avis :

Ne faites pas cela ! Cela annule toute optimisation des performances car TodoApp effectuera un nouveau rendu après chaque action. Il est préférable d'avoir un connect() plus granulaire sur plusieurs composants de votre hiérarchie de vues qui, chacun, n'écoute qu'une tranche pertinente de l'état. écoutent seulement une tranche pertinente de l'état.

Nous vous déconseillons d'utiliser ce modèle. Plutôt, chaque connexion <Repo> spécifiquement pour qu'il lise ses propres données dans son mapStateToProps . Le " vue sur l'arbre L'exemple de l'"exemple" montre comment procéder.

Si vous rendez la forme de l'État plus normalisé (pour l'instant, tout est imbriqué), vous pouvez séparer les éléments suivants repoIds de reposById et ensuite seulement votre RepoList re-rendu si repoIds changement. De cette façon, les modifications apportées aux dépôts individuels n'affecteront pas la liste elle-même, et seuls les dépôts correspondants seront modifiés. Repo sera rendu à nouveau. Cette demande de retrait pourrait vous donner une idée de comment cela pourrait fonctionner. Le " le monde réel "Cet exemple montre comment écrire des réducteurs qui traitent des données normalisées.

Notez que pour bénéficier réellement des performances offertes par la normalisation de l'arbre, vous devez faire exactement ce qui suit cette demande de retrait et passer un mapStateToProps() l'usine à 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 ID ne changent jamais. Utilisation de ownProps s'accompagne d'une pénalité de performance : les accessoires internes doivent être recalculés chaque fois que les accessoires externes changent. Cependant, l'utilisation de initialOwnProps ne subit pas cette pénalité car il n'est utilisé 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() sur ConnectedRepo pour utiliser une usine avec initialOwnProps plutôt que ownProps . Cela permet à React Redux d'éviter la réévaluation des accessoires.

J'ai également supprimé l'inutile shouldComponentUpdate() sur le <Repo> parce que React Redux se charge 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 devez afficher une telle tonne de données, elles ne peuvent de toute façon pas tenir dans l'écran. Dans ce cas, une meilleure solution est d'utiliser une balise table virtuelle de sorte que vous pouvez rendre des milliers de lignes sans la surcharge de performance de les afficher réellement.


J'ai trouvé une solution en remplaçant tous les tags par un observable dans le reducer.

Si ça a des effets secondaires, ce n'est pas un réducteur de 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 les sujets.

1 votes

Je veux juste clarifier l'utilisation de connect((state) => state)(RepoList) . Ici, je n'ai que repos dans l'arbre d'état, ce ne sera donc pas un problème. Mais oui, si l'arbre d'état est plus complexe, cela serait moins performant.

0 votes

Donc attendez, pour "utiliser" initialOwnProps, j'ai juste besoin de m'assurer que la première arg de connect() est une fonction retournant une fonction retournant l'objet props plutôt qu'une simple fonction qui retourne l'objet props directement ? Et tout cela est déterminé paresseusement lors de la première invocation ?

0 votes

Merci, votre réponse a résolu mon problème. J'ai également mis à jour la question (Update 2) avec de nouvelles démos.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X