web-dev-qa-db-fra.com

Une application React-Redux peut-elle vraiment évoluer ainsi que, par exemple, Backbone? Même avec resélection. Sur le mobile

Dans Redux, chaque modification du magasin déclenche un notify sur tous les composants connectés. Cela rend les choses très simples pour le développeur, mais que faire si vous avez une application avec N composants connectés et que N est très grand?

Chaque modification du magasin, même si elle n'est pas liée au composant, exécute toujours un shouldComponentUpdate avec un simple === test sur les chemins reselected du magasin. C'est rapide, non? Bien sûr, peut-être une fois. Mais N fois, pour chaque changement? Ce changement fondamental dans la conception me fait remettre en question la véritable évolutivité de Redux.

Comme optimisation supplémentaire, on peut regrouper tous les appels notify en utilisant _.debounce. Même ainsi, ayant N === des tests pour chaque changement de magasin et la gestion d'une autre logique, par exemple la logique de visualisation, semblent être un moyen de parvenir à une fin.

Je travaille sur une application hybride mobile-web sociale de santé et de fitness avec des millions d'utilisateurs et je passe de Backbone à Redux. Dans cette application, un utilisateur est présenté avec une interface glissable qui lui permet de naviguer entre différentes piles de vues, similaire à Snapchat, sauf que chaque pile a une profondeur infinie. Dans le type de vue le plus populaire, une roulette sans fin gère efficacement le chargement, le rendu, la fixation et le détachement des éléments d'alimentation, comme un poteau. Pour un utilisateur engagé, il n'est pas rare de parcourir des centaines ou des milliers de publications, puis d'entrer le flux d'un utilisateur, puis le flux d'un autre utilisateur, etc. Même avec une optimisation poussée, le nombre de composants connectés peut devenir très important.

Désormais, la conception de Backbone permet à chaque vue d'écouter précisément les modèles qui l'affectent, réduisant N à une constante.

Suis-je en train de manquer quelque chose, ou Redux est-il fondamentalement défectueux pour une grande application?

51
Garrett

Ce n'est pas un problème inhérent à Redux IMHO.

Soit dit en passant, au lieu d'essayer de rendre des composants de 100k en même temps, vous devriez essayer de les simuler avec une lib comme react-infinite ou quelque chose de similaire, et ne rendre que le visible (ou proche d'être ) des éléments de votre liste. Même si vous réussissez à rendre et à mettre à jour une liste de 100k, ce n'est toujours pas performant et cela prend beaucoup de mémoire. Voici quelques conseils LinkedIn

Cette réponse considérera que vous essayez toujours de rendre 100 000 éléments pouvant être mis à jour dans votre DOM et que vous ne voulez pas que des écouteurs de 100 000 (store.subscribe()) soient appelés à chaque changement.


2 écoles

Lorsque vous développez une application d'interface utilisateur de manière fonctionnelle, vous avez essentiellement deux choix:

Rendre toujours du haut

Cela fonctionne bien mais implique plus de passe-partout. Ce n'est pas exactement la méthode Redux suggérée mais est réalisable, avec quelques inconvénients . Notez que même si vous parvenez à avoir une seule connexion redux, vous devez toujours appeler beaucoup de shouldComponentUpdate à de nombreux endroits. Si vous avez une pile infinie de vues (comme une récursivité), vous devrez également rendre en tant que dom virtuel toutes les vues intermédiaires et shouldComponentUpdate sera appelé sur beaucoup d'entre elles. Ce n'est donc pas vraiment plus efficace même si vous avez une seule connexion.

Si vous ne prévoyez pas d'utiliser les méthodes de cycle de vie React mais utilisez uniquement des fonctions de rendu pur, alors vous devriez probablement envisager d'autres options similaires qui ne se concentreront que sur ce travail, comme dek (qui peut être utilisé avec Redux)

D'après ma propre expérience, le faire avec React n'est pas assez performant sur les appareils mobiles plus anciens (comme mon Nexus4), en particulier si vous liez des entrées de texte à votre état atom .

Connexion de données à des composants enfants

C'est ce que react-redux suggère en utilisant connect. Ainsi, lorsque l'état change et qu'il est uniquement lié à un enfant plus profond, vous ne restituez que cet enfant et vous n'avez pas à rendre les composants de niveau supérieur à chaque fois comme les fournisseurs de contexte (redux/intl/custom ...) ni la disposition principale de l'application. Vous évitez également d'appeler shouldComponentUpdate sur d'autres enfants car il est déjà intégré dans l'écouteur. Appeler beaucoup d'auditeurs très rapides est probablement plus rapide que de rendre à chaque fois des composants de réaction intermédiaires, et cela permet également de réduire beaucoup de passe-partout passant par les accessoires, donc pour moi, cela a du sens lorsqu'il est utilisé avec React.

Notez également que la comparaison d'identité est très rapide et que vous pouvez en faire facilement beaucoup à chaque changement. Rappelez-vous la sale vérification d'Angular: certaines personnes ont réussi à créer de vraies applications avec ça! Et la comparaison d'identité est beaucoup plus rapide.


Comprendre votre problème

Je ne suis pas sûr de comprendre parfaitement tout votre problème, mais je comprends que vous avez des vues avec des éléments similaires à 100k et vous vous demandez si vous devez utiliser connect avec tous ces éléments à 100k car appeler 100k auditeurs à chaque changement semble coûteux.

Ce problème semble inhérent à la nature de la programmation fonctionnelle avec l'interface utilisateur: la liste a été mise à jour, vous devez donc restituer la liste, mais malheureusement c'est une liste très longue et elle semble inefficace ... Avec Backbone, vous pouvez pirater quelque chose pour rendre seulement l'enfant. Même si vous restituez cet enfant avec React, vous déclencherez le rendu de manière impérative au lieu de simplement déclarer "lorsque la liste change, restituez-la").


Résoudre votre problème

Évidemment, la connexion des éléments de la liste 100k semble pratique mais n'est pas performante en raison de l'appel d'écouteurs 100k react-redux, même s'ils sont rapides.

Maintenant, si vous connectez la grande liste d'éléments de 100k au lieu de chaque élément individuellement, vous n'appelez qu'un seul écouteur react-redux, puis devez rendre cette liste de manière efficace.


Solution naïve

Itération sur les 100k éléments pour les rendre, conduisant à 99999 éléments retournant faux dans shouldComponentUpdate et un seul re-rendu:

list.map(item => this.renderItem(item))

Solution performante 1: connect + optimiseur de magasin personnalisé

La méthode connect de React-Redux n'est qu'un composant d'ordre supérieur (HOC) qui injecte les données dans le composant encapsulé. Pour ce faire, il enregistre un écouteur store.subscribe(...) pour chaque composant connecté.

Si vous souhaitez connecter 100 000 éléments d'une même liste, c'est un chemin critique de votre application qui mérite d'être optimisé. Au lieu d'utiliser le connect par défaut, vous pouvez créer le vôtre.

  1. Enhancer de magasin

Exposer une méthode supplémentaire store.subscribeItem(itemId,listener)

Enveloppez dispatch de sorte que chaque fois qu'une action liée à un élément est envoyée, vous appelez le ou les écouteurs enregistrés de cet élément.

Une bonne source d'inspiration pour cette implémentation peut être redux-batched-subscribe .

  1. Connexion personnalisée

Créez un composant d'ordre supérieur avec une API comme:

Item = connectItem(Item)

Le HOC peut s'attendre à une propriété itemId. Il peut utiliser le magasin amélioré Redux à partir du contexte React puis enregistrer son écouteur: store.subscribeItem(itemId,callback). Le code source du connect d'origine peut servir de base inspiration.

  1. Le HOC ne déclenchera un nouveau rendu que si l'élément change

Réponse connexe: https://stackoverflow.com/a/34991164/82609

Problème connexe de react-redux: https://github.com/rackt/react-redux/issues/269

Solution performante 2: écoute des événements dans les composants enfants

Il peut également être possible d'écouter les actions Redux directement dans les composants, en utilisant redux-dispatch-subscribe ou quelque chose de similaire, de sorte qu'après le premier rendu de la liste, vous écoutez les mises à jour directement dans le composant item et remplaciez données originales de la liste des parents.

class MyItemComponent extends Component {
  state = {
    itemUpdated: undefined, // Will store the local
  };
  componentDidMount() {
    this.unsubscribe = this.props.store.addDispatchListener(action => {
      const isItemUpdate = action.type === "MY_ITEM_UPDATED" && action.payload.item.id === this.props.itemId;
      if (isItemUpdate) {
        this.setState({itemUpdated: action.payload.item})
      }
    })
  }
  componentWillUnmount() {
    this.unsubscribe();
  }
  render() {
    // Initially use the data provided by the parent, but once it's updated by some event, use the updated data
    const item = this.state.itemUpdated || this.props.item;
    return (
      <div>
        {...}
      </div>
    );
  }
}

Dans ce cas, redux-dispatch-subscribe Peut ne pas être très performant car vous créeriez quand même des abonnements de 100k. Vous préférez créer votre propre middleware optimisé similaire à redux-dispatch-subscribe Avec une API comme store.listenForItemChanges(itemId), stockant les écouteurs d'élément sous forme de carte pour une recherche rapide des écouteurs corrects à exécuter ...


Solution performante 3: essais vectoriels

Une approche plus performante envisagerait d'utiliser une structure de données persistante comme un trie vectoriel :

Trie

Si vous représentez votre liste d'éléments de 100k comme un trie, chaque nœud intermédiaire a la possibilité de court-circuiter le rendu plus tôt, ce qui permet d'éviter beaucoup de shouldComponentUpdate chez les enfants.

Cette technique peut être utilisée avec ImmutableJS et vous pouvez trouver quelques expériences que j'ai faites avec ImmutableJS: Performances de réaction: rendre une grande liste avec PureRenderMixin Elle a cependant des inconvénients comme le font les bibliothèques comme ImmutableJs n'expose pas encore les API publiques/stables pour le faire ( issue ), et ma solution pollue le DOM avec des nœuds intermédiaires <span> inutiles ( issue ).

Voici un JsFiddle qui montre comment une liste ImmutableJS de 100k éléments peut être rendue efficacement. Le rendu initial est assez long (mais je suppose que vous n'initialisez pas votre application avec 100 000 éléments!) Mais après, vous pouvez remarquer que chaque mise à jour ne conduit qu'à une petite quantité de shouldComponentUpdate. Dans mon exemple, je ne mets à jour le premier élément que toutes les secondes, et vous remarquez que même si la liste contient 100 000 éléments, cela ne nécessite que quelque chose comme 110 appels à shouldComponentUpdate, ce qui est beaucoup plus acceptable! :)

Edit : il semble qu'ImmutableJS n'est pas si bon pour conserver sa structure immuable sur certaines opérations, comme l'insertion/suppression d'éléments à un index aléatoire. Voici un JsFiddle qui montre les performances que vous pouvez attendre en fonction de l'opération sur la liste. Étonnamment, si vous voulez ajouter de nombreux éléments à la fin d'une grande liste, appeler plusieurs fois list.Push(value) semble préserver beaucoup plus la structure arborescente que d'appeler list.concat(values).

Soit dit en passant, il est documenté que la liste est efficace lors de la modification des bords. Je ne pense pas que ces mauvaises performances sur l'ajout/la suppression à un index donné soient liées à ma technique mais plutôt à l'implémentation sous-jacente de la liste ImmutableJs.

Les listes implémentent Deque, avec des ajouts et des suppressions efficaces de la fin (Push, pop) et du début (unshift, shift).

87
Sebastien Lorber

Cela peut être une réponse plus générale que celle que vous recherchez, mais en gros:

  1. La recommandation des documents Redux est de connecter React composants assez haut dans la hiérarchie des composants. Voir cette section. . Cela permet de gérer le nombre de connexions, et vous pouvez puis passez simplement les accessoires mis à jour dans les composants enfants.
  2. Une partie de la puissance et de l'évolutivité de React vient du fait d'éviter le rendu des composants invisibles. Par exemple, au lieu de définir une classe invisible sur un élément DOM, dans React nous ne rendons pas le composant du tout. Le rendu des composants qui n'ont pas changé n'est pas du tout un problème, car le processus de différenciation DOM virtuel optimise les interactions DOM de bas niveau.
5
mjhm