Rendre React réactif : la quête d'applications React performantes et faciles à maintenir | Mendix

Passer au contenu principal

Rendre React réactif : la quête d'applications React performantes et faciles à maintenir

Contexte du blog sur le développement d'applications

Édition 2-3-2016 : Mobservable a été rebaptisé MobX

Comment créer des applications React ultra rapides ? Récemment, nous avons commencé à utiliser React dans l'un de nos projets à grande échelle et cela nous a beaucoup aidé grâce à sa manière structurée de créer des composants et à son DOM virtuel rapide qui permet d'économiser des tonnes de mises à jour de l'interface utilisateur. La beauté de ce projet est qu'il présente de nombreux défis intéressants ; il doit dessiner des milliers d'objets dans le navigateur et ces objets sont fortement couplés les uns aux autres. Les valeurs d'un objet peuvent être utilisées dans une quantité arbitraire d'autres objets, de sorte que de petits changements peuvent nécessiter des mises à jour dans de nombreuses parties non liées de l'interface utilisateur. Ces valeurs peuvent être mises à jour par des actions de glisser-déposer de l'utilisateur, donc pour que l'interface utilisateur reste réactive, toutes les mises à jour et repeintures doivent se produire en moins de 40 millisecondes. Et bien que React soit rapide, il ne nous a pas fallu longtemps pour comprendre que React seul ne ferait pas le travail.

Nous avons donc commencé à chercher une solution qui nous offrirait les performances nécessaires tout en permettant à notre base de code d'être maintenable selon les principes de React. En bref, nous voulions une véritable solution. Nous avons donc essayé de tirer parti d'un concept issu du monde de la programmation fonctionnelle réactive, à savoir observables. L'argument de vente des observables est que tous les calculs détectent automatiquement les autres observables qu'ils utilisent. Le calcul sera ensuite réévalué automatiquement lorsque l'un de ces observables changera à l'avenir. observables sont un concept utilisé dans d'autres frameworks d'interface utilisateur, tels que Ember et Knockout. Nous avons compris que si tous nos objets de modèle devenaient observables et tous nos composants React sont devenus Observateurs du modèle, nous n'aurions pas besoin d'appliquer de magie supplémentaire pour nous assurer que la partie pertinente, et uniquement la partie pertinente, de notre interface utilisateur soit mise à jour. Lisez la suite pour voir toutes les merveilles révélées. Il y a même des chiffres vers la fin !

Commençons par un exemple artificiel pour rendre cela beaucoup moins théorique (ou moins compliqué si vous préférez). Imaginez une application React qui représente une petite boutique. Il y a quelques articles, et il y a un panier dans lequel vous pouvez mettre certains de ces articles. Quelque chose comme ça :

Capture d'écran d'un exemple d'application

Pouf, c'est dans la vraie vie.

Le modèle de données

Pour commencer, définissons le modèle de données. Il y a des articles avec un nom et un prix et il y a un panier avec un coût total, qui est basé sur la somme de ses entrées. Chaque entrée fait référence à un article, stocke une quantité et a un prix dérivé. Les relations au sein de notre modèle de données sont visualisées ci-dessous. Les puces ouvertes représentent les données dérivées qui doivent être mises à jour si d'autres données changent, tout comme leur représentation dans l'interface utilisateur. Ainsi, même dans ce modèle simple, beaucoup de données circulent et de nombreuses mises à jour de l'interface utilisateur sont nécessaires lorsque des éléments changent.

Graphique du modèle de données

Compilons une liste d’exigences :

  • Si le prix d'un article change, toutes les entrées du panier d'achat associées doivent réévaluer leur prix.
  • .. et le coût total du chariot devrait également l'être.
  • Si la quantité d'articles dans le panier change, les coûts totaux doivent être mis à jour.
  • Si un article est renommé, sa vue doit être mise à jour
  • Si un article est renommé, la vue des entrées de panier associées doit être mise à jour
  • Si un nouvel article est ajouté au panier.
  • etc ..

L'essentiel de nos problèmes d'interface utilisateur est probablement clair à présent. En tant que programmeur, vous ne souhaitez pas écrire de code standard pour traiter toutes sortes de mises à jour possibles, mais votre utilisateur risque de devoir attendre très longtemps si votre application est toujours réaffichée à chaque modification de données.

Alors, résolvons ce problème une fois pour toutes et écrivons notre modèle de données :

Ok, ce n'était pas trop difficile, n'est-ce pas ? Les fonctions de constructeur ci-dessus s'appuient fortement sur MobX bibliothèque, qui fournit une implémentation autonome du concept observable (elle devrait se combiner avec d'autres bibliothèques basées sur JavaScript aussi facilement qu'avec React). props La fonction crée de nouvelles propriétés observables sur l'objet cible, en fonction des clés fournies et des types de valeurs. En raison de la nature observable de toutes les propriétés, les fonctions ci-dessus sont automatiquement (et uniquement) mises à jour lorsque certaines de ses dépendances changent. Cela répond immédiatement à certaines de nos exigences, comme par exemple total du panier est automatiquement mis à jour lorsque de nouvelles entrées sont ajoutées, lorsque le prix des articles change, etc.

L'interface utilisateur

Il faut voir pour le croire. Construisons donc une interface utilisateur autour de ce modèle. Nous créons des composants React qui restituent nos données initiales. L'extrait JSX suivant montre une vue sur le panier d'achat, restitue toutes les entrées du panier et affiche le prix total du panier. Comme vous pouvez l'imaginer, d'autres composants de l'application, comme la vue sur les articles, sont très similaires.

Plutôt simple, non ? Un composant CartView reçoit un panier, affiche son total et ses éléments individuels à l'aide de CartEntryView, qui à son tour imprime le nom de l'article associé et la quantité d'articles souhaitée. Selon les meilleures pratiques de React, chaque élément répertorié doit être identifiable de manière unique, nous attribuons donc un identifiant arbitraire mais immuable à chaque entrée. Le bouton de suppression réduit ce montant d'un et s'il atteint zéro, l'entrée entière est supprimée du panier. Notez que nulle part dans le removeArticle fonction avons-nous indiqué que l'interface utilisateur devait être mise à jour.

La prochaine grande étape consiste à forcer ces composants à rester à jour avec le modèle de données, par exemple lorsque l'entrée est supprimée. Comme vous pouvez le déterminer facilement à partir du code de rendu, il existe de nombreuses transitions de données possibles ; le nombre d'articles peut changer, le coût total du panier peut changer, le nom d'un article peut changer, même la référence entre l'entrée et l'article peut changer. Comment allons-nous écouter tous ces changements ?

Eh bien, c'est assez simple. Il suffit d'utiliser mobxReact.observer du mobx-react package à chaque composant et cela suffit à régler toutes nos autres exigences :

Attendez, quoi, c'est tout ? Oui, regardez simplement la démo et la source de ce qui précède sur JSviolon. Alors que s'est-il passé ici ? observer La fonction a fait deux choses pour nous. Tout d'abord, elle a transformé la fonction de rendu du composant en fonction observable. Deuxièmement, le composant lui-même a été enregistré comme observateur de cette fonction, de sorte qu'à chaque fois que le rendu devient obsolète, un nouveau rendu est forcé. Ainsi, cette fonction (et le décorateur si vous utilisez ES6) garantit que chaque fois que des données observables sont modifiées, seules les parties pertinentes de l'interface utilisateur sont mises à jour. Jouez simplement avec l'exemple d'application et, tout en faisant cela, gardez un œil sur le panneau de journal et voyez comment l'interface utilisateur est mise à jour en fonction de vos actions réelles et des données réelles :

  • essayer de renommer un article qui n'est pas dans le panier
  • ajouter un article au panier, puis le renommer
  • ajouter un article au panier et mettre à jour son prix
  • retirez-le du panier, mettez à nouveau son prix à jour
  • … etc. Vous remarquerez qu’à chaque action, le nombre minimum de composants sera ré-affiché.

Enfin, comme chaque composant suit ses propres dépendances, il n'est généralement pas nécessaire de restituer explicitement les enfants d'un composant. Par exemple, si le total du panier d'achat est restitué, il n'est pas nécessaire de restituer également les entrées. Mixage PureRender veille à ce que cela n'arrive pas.

Les chiffres

Alors, qu'avons-nous obtenu ? À titre de comparaison, ici vous pouvez trouver exactement la même application mais sans observables et avec une approche naïve de « re-render-all-the-things ». Avec seulement quelques articles, vous ne remarquerez aucune différence, mais une fois que le nombre d'articles augmente, la différence de performance devient vraiment significative.

 

Création et rendu d'un graphique d'articlesMise à jour du tableau des articles

La création de grandes quantités de données et de composants se comporte de manière très similaire avec et sans observables. Mais une fois les données modifiées, les observables commencent vraiment à briller. La mise à jour de 10 articles dans une collection de 10,000 2.5 éléments est environ dix fois plus rapide ! 250 secondes sont tombées à 10 millisecondes. C'est la différence entre une expérience avec et sans retard. D'où vient cette différence ? Examinons d'abord les rapports de rendu React après l'exécution des mises à jour dans le scénario « Mettre à jour 10000 articles dans une liste de XNUMX XNUMX articles » sans observables :

Capture d'écran du journal

Comme vous pouvez le voir, les vingt mille ArticlesViews et CartEntryViews sont tous rendus à nouveau. Cependant, selon React, 2,145 2,433 des XNUMX XNUMX millisecondes de temps de rendu total ont été gaspillées. Gaspillage signifie : temps passé à exécuter des fonctions de rendu qui n'ont pas réellement abouti à une mise à jour du DOM réel. Cela suggère fortement que le fait de tout restituer de manière naïve est une grosse perte de temps CPU s'il y a beaucoup de composants. À titre de comparaison, voici le rapport du même scénario lorsque des observables sont utilisés :

Capture d'écran du journal

C'est une grande différence ! Au lieu de restituer 20,006 31 composants, seuls XNUMX composants sont restitués. Et plus important encore, aucun gaspillage n'est signalé ! Cela signifie que chaque composant restitué a réellement modifié quelque chose dans le DOM. C'est exactement ce que nous voulions réaliser en utilisant des observables !

D'après le rapport, il apparaît clairement que la majeure partie du temps de rendu restant, 243 des 267 millisecondes au total, est consacrée au rendu du CartView, qui est restitué uniquement pour actualiser les coûts totaux du panier. Cependant, le nouveau rendu du CartView implique également que les dix mille entrées sont revisitées pour voir si l'un des arguments du CartEntryViews a changé. Ainsi, en plaçant simplement le total du CartView dans son propre composant, CartTotalView, l'ensemble du rendu du CartView peut être ignoré si seul le total des coûts change. Cela réduit encore davantage notre temps de rendu à environ 60 millisecondes (voir la série « optimisée » dans le graphique ci-dessus). C'est environ 40 fois plus rapide que la même mise à jour dans notre application React vanilla !

Conclusion

En utilisant des observables, nous avons créé une application qui est d'un ordre de grandeur plus rapide que la même application qui restitue naïvement tous les composants. Et, ce qui est aussi important (pour vous en tant que programmeur), nous l'avons fait sans compromettre la maintenabilité du code. Jetez simplement un œil au code source des deux JSFiddles liés ci-dessus ; les deux listes sont très similaires et sont toutes deux aussi pratiques à utiliser.

Aurions-nous pu obtenir le même résultat avec d’autres techniques ? Peut-être. Par exemple, il existe ImmutableJS, qui rend également le rendu React très rapide en mettant à jour uniquement les composants qui reçoivent des données modifiées. Cependant, vous devez faire des concessions beaucoup plus importantes sur votre modèle de données. Après tout, à mon humble avis, les classes mutables sont au final un peu plus pratiques à utiliser que leurs homologues immuables. De plus, les structures de données immuables ne vous aident pas à maintenir vos valeurs calculées à jour. Ainsi, avec des données immuables, changer le nom d’un article rendrait très rapidement l’ArticleView, mais n’invaliderait toujours pas les CartEntryViews existantes qui font référence au même article.

Une autre technique que l'on peut appliquer pour optimiser une application React consiste à créer des événements pour chaque mutation possible dans les données et à (dé)enregistrer des écouteurs pour ces événements aux bons moments et dans les bons composants. Mais cela donne lieu à des tonnes de code standard qui est sujet à des erreurs de maintenance. De plus, je pense que je suis tout simplement trop paresseux pour faire ce genre de choses.

Au fait, je vous conseille fortement d'utiliser des contrôleurs ou des répartiteurs d'actions comme abstraction autour de la mise à jour des données de votre modèle pour garder une séparation claire des préoccupations dans votre projet.

Pour conclure, dans les grands projets, combiner React avec Observables a si bien fonctionné que j'ai parfois vu des mutations de données mettre à jour correctement l'interface utilisateur dans des cas particuliers auxquels je n'avais même pas pensé, sans rencontrer de problèmes de performances. Donc pour ma part, je laisserai le travail difficile de déterminer quand et comment mettre à jour l'interface utilisateur le plus rapidement possible à React et Observables, et me concentrerai sur les parties intéressantes du codage :).

Discutez de cet article sur Hacker Nouvelles.

Ressources

Choisissez votre langue