Comprendre le comportement de rendu dans React

Publié: 2020-11-16

Avec la vie, la mort, le destin et les impôts, le comportement de rendu de React est l'une des plus grandes vérités et mystères de la vie.

Plongeons-nous !

Comme tout le monde, j'ai commencé mon parcours de développement front-end avec jQuery. La manipulation DOM basée sur JS pur était un cauchemar à l'époque, c'était donc ce que tout le monde faisait. Puis lentement, les frameworks basés sur JavaScript sont devenus si importants que je ne pouvais plus les ignorer.

Le premier que j'ai appris était Vue. J'ai eu énormément de mal parce que les composants, l'état et tout le reste étaient un modèle mental totalement nouveau, et c'était très pénible de tout intégrer. Mais finalement, je l'ai fait et je me suis félicité. Félicitations, mon pote, me disais-je, tu as fait la montée raide; maintenant, le reste des cadres, si jamais vous avez besoin de les apprendre, sera très facile.

Alors, un jour, quand j'ai commencé à apprendre React, j'ai réalisé à quel point j'avais terriblement tort. Facebook n'a pas facilité les choses en lançant des crochets et en disant à tout le monde : « Hé, utilisez-le à partir de maintenant. Mais ne réécrivez pas les classes ; les cours se passent bien. En fait, pas tellement, mais ça va. Mais les crochets sont tout, et ils sont l'avenir.

J'ai compris? Super!".

Finalement, j'ai traversé cette montagne aussi. Mais ensuite, j'ai été frappé par quelque chose d'aussi important et difficile que React lui-même : le rendu .

Surprendre!!!

Si vous avez rencontré le rendu et ses mystères dans React, vous savez de quoi je parle. Et si ce n'est pas le cas, vous n'avez aucune idée de ce qui vous attend !

Mais avant de perdre du temps sur quoi que ce soit, c'est une bonne habitude de se demander ce que vous y gagneriez (contrairement à moi, qui suis un idiot surexcité et qui apprendra n'importe quoi pour le plaisir). Si votre vie de développeur React se déroule bien sans vous soucier de ce qu'est ce rendu, pourquoi s'en soucier ? Bonne question, alors répondons d'abord à celle-ci, puis nous verrons ce qu'est réellement le rendu.

Pourquoi est-il important de comprendre le comportement de rendu dans React ?

Nous commençons tous à apprendre React en écrivant des composants (de nos jours, fonctionnels) qui renvoient quelque chose appelé JSX. Nous comprenons également que ce JSX est en quelque sorte converti en éléments HTML DOM réels qui apparaissent sur la page. Les pages sont mises à jour au fur et à mesure que l'état est mis à jour, les itinéraires changent comme prévu et tout va bien. Mais cette vision du fonctionnement de React est naïve et source de nombreux problèmes.

Bien que nous réussissions souvent à écrire des applications complètes basées sur React, il y a des moments où nous trouvons certaines parties de notre application (ou l'ensemble de l'application) remarquablement lentes. Et le pire. . . nous n'avons pas un seul indice pourquoi! Nous avons tout fait correctement, nous ne voyons aucune erreur ou avertissement, nous avons suivi toutes les bonnes pratiques de conception de composants, de normes de codage, etc., et aucune lenteur du réseau ou calcul de logique métier coûteux ne se produit en coulisses.

Parfois, c'est un problème totalement différent : il n'y a rien de mal avec les performances, mais l'application se comporte bizarrement. Par exemple, faire trois appels d'API au backend d'authentification mais un seul à tous les autres. Ou certaines pages sont redessinées deux fois, la transition visible entre les deux rendus de la même page créant une UX discordante.

Oh non! Pas encore!!

Pire encore, il n'y a pas d'aide externe disponible dans des cas comme ceux-ci. Si vous allez sur votre forum de développement préféré et posez cette question, ils vous répondront : « Je ne peux pas le dire sans regarder votre application. Pouvez-vous joindre un exemple de travail minimum ici ? » Eh bien, bien sûr, vous ne pouvez pas attacher l'intégralité de l'application pour des raisons juridiques, alors qu'un petit exemple de travail de cette partie peut ne pas contenir ce problème car il n'interagit pas avec l'ensemble du système comme il l'est dans l'application réelle.

Foutu ? Ouais, si vous me demandez.

Donc, à moins que vous ne vouliez voir de tels jours de malheur, je vous suggère de développer une compréhension – et un intérêt, je dois insister ; la compréhension acquise à contrecœur ne vous mènera pas loin dans le monde de React - dans cette chose mal comprise appelée rendu dans React. Croyez-moi, ce n'est pas si difficile à comprendre, et même si c'est très difficile à maîtriser, vous irez très loin sans avoir à connaître tous les coins et recoins.

Que signifie rendu dans React ?

C'est, mon ami, une excellente question. Nous n'avons pas tendance à le demander lorsque nous apprenons React (je le sais parce que je ne l'ai pas fait) parce que le mot "rendre" nous berce peut-être dans un faux sentiment de familiarité. Bien que la signification du dictionnaire soit complètement différente (et ce n'est pas important dans cette discussion), nous, les programmeurs, avons déjà une idée de ce que cela devrait signifier. Travailler avec des écrans, des API 3D, des cartes graphiques et lire les spécifications des produits entraîne notre esprit à penser à quelque chose comme "peindre une image" lorsque nous lisons le mot "rendre". Dans la programmation du moteur de jeu, il y a un moteur de rendu, dont le seul travail est de - précisément !, peindre le monde tel que remis par la scène.

Et donc nous pensons que lorsque React "rend" quelque chose, il collecte tous les composants et repeint le DOM de la page Web. Mais dans le monde React (et oui, même dans la documentation officielle), ce n'est pas de cela qu'il s'agit. Alors, serrons nos ceintures de sécurité et plongeons vraiment profondément dans les composants internes de React.

"Que je sois damné . . .”

Vous devez avoir entendu dire que React maintient ce qu'on appelle un DOM virtuel et qu'il le compare périodiquement avec le DOM réel et applique les modifications nécessaires (c'est pourquoi vous ne pouvez pas simplement ajouter jQuery et React ensemble - React doit prendre le contrôle total de les DOM). Maintenant, ce DOM virtuel n'est pas composé d'éléments HTML comme le fait le vrai DOM, mais d'éléments React. Quelle est la différence? Bonne question! Pourquoi ne pas créer une petite application React et voir par nous-mêmes ?

J'ai créé cette application React très simple à cet effet. Le code entier est juste un seul fichier contenant quelques lignes :

 import React from "react"; import "./styles.css"; export default function App() { const element = ( <div className="App"> <h1>Hello, there!</h1> <h2>Let's take a look inside React elements</h2> </div> ); console.log(element); return element; }

Remarquez ce que nous faisons ici?

Oui, il suffit de consigner à quoi ressemble un élément JSX. Ces expressions et composants JSX sont quelque chose que nous avons écrit des centaines de fois, mais nous prêtons rarement attention à ce qui se passe. Si vous ouvrez la console de développement de votre navigateur et exécutez cette application, vous verrez un Object qui se développe en :

Cela peut sembler intimidant, mais notez quelques détails intéressants :

  • Ce que nous examinons est un objet JavaScript ordinaire et non un nœud DOM.
  • Notez que la propriété props indique qu'elle a un className d' App (qui est la classe CSS définie dans le code) et que cet élément a deux enfants (cela correspond également, les éléments enfants étant les balises <h1> et <h2> ) .
  • La propriété _source nous indique où le code source commence le corps de l'élément. Comme vous pouvez le voir, il nomme le fichier App.js comme source et mentionne le numéro de ligne 6. Si vous regardez à nouveau le code, vous constaterez que la ligne 6 se trouve juste après la balise JSX d'ouverture, ce qui est logique. Les parenthèses JSX contiennent l'élément React ; ils n'en font pas partie, car ils servent à se transformer en un appel React.createElement() plus tard.
  • La propriété __proto__ nous dit que cet objet dérive tous ses. properties de la racine JavaScript Object , renforçant à nouveau l'idée que ce ne sont que des objets JavaScript de tous les jours que nous examinons ici.

Donc, maintenant, nous comprenons que le soi-disant DOM virtuel ne ressemble en rien au vrai DOM mais est un arbre d'objets React (JavaScript) représentant l'interface utilisateur à ce moment-là.

*SOUPIR* . . . Sommes-nous déjà là?

Épuisé?

Croyez-moi, je le suis aussi. Tourner ces idées encore et encore dans ma tête pour essayer de les présenter de la meilleure façon possible, puis penser aux mots pour les faire ressortir et les réorganiser — n'est pas facile.

Mais nous sommes distraits !

Ayant survécu jusqu'ici, nous sommes maintenant en mesure de répondre à la question que nous recherchions : qu'est-ce que le rendu dans React ?

Eh bien, le rendu est le processus du moteur React qui parcourt le DOM virtuel et collecte l'état actuel, les accessoires, la structure, les modifications souhaitées dans l'interface utilisateur, etc. React met maintenant à jour le DOM virtuel à l'aide de certains calculs et compare également le nouveau résultat avec le DOM réel sur la page. Ce calcul et cette comparaison sont ce que l'équipe React appelle officiellement "réconciliation", et si vous êtes intéressé par leurs idées et leurs algorithmes pertinents, vous pouvez consulter la documentation officielle.

Il est temps de s'engager !

Une fois la partie rendu effectuée, React entame une phase appelée « commit », durant laquelle il applique les modifications nécessaires au DOM. Ces modifications sont appliquées de manière synchrone (l'une après l'autre, bien qu'un nouveau mode fonctionnant simultanément soit attendu prochainement), et le DOM est mis à jour. Exactement quand et comment React applique ces changements n'est pas notre préoccupation, car c'est quelque chose qui est totalement sous le capot et susceptible de continuer à changer à mesure que l'équipe React essaie de nouvelles choses.

Rendu et performances dans les applications React

Nous avons maintenant compris que le rendu signifie la collecte d'informations et qu'il n'est pas nécessaire qu'il entraîne des modifications visuelles du DOM à chaque fois. Nous savons également que ce que nous considérons comme un « rendu » est un processus en deux étapes impliquant le rendu et la validation. Nous allons maintenant voir comment le rendu (et plus important encore, le re-rendu) est déclenché dans les applications React et comment le fait de ne pas connaître les détails peut entraîner un mauvais fonctionnement des applications.

Nouveau rendu en raison d'un changement dans le composant parent

Si un composant parent dans React change (par exemple, parce que son état ou ses accessoires ont changé), React parcourt l'ensemble de l'arborescence de cet élément parent et restitue tous les composants. Si votre application comporte de nombreux composants imbriqués et de nombreuses interactions, vous subissez sans le savoir un énorme impact sur les performances chaque fois que vous modifiez le composant parent (en supposant que ce n'est que le composant parent que vous vouliez modifier).

Certes, le rendu n'entraînera pas React à modifier le DOM réel car, lors de la réconciliation, il détectera que rien n'a changé pour ces composants. Mais, c'est toujours du temps CPU et de la mémoire gaspillée, et vous seriez surpris de la rapidité avec laquelle cela s'additionne.

Nouveau rendu en raison d'un changement de contexte

La fonctionnalité Context de React semble être l'outil de gestion d'état préféré de tout le monde (ce pour quoi il n'a pas du tout été conçu). C'est tellement pratique - il suffit d'envelopper le composant le plus haut dans le fournisseur de contexte, et le reste est simple ! La majorité des applications React sont conçues comme ceci, mais si vous avez lu cet article jusqu'à présent, vous avez probablement repéré ce qui ne va pas. Oui, chaque fois que l'objet de contexte est mis à jour, il déclenche un nouveau rendu massif de tous les composants de l'arbre.

La plupart des applications n'ont aucune conscience des performances, donc personne ne le remarque, mais comme indiqué précédemment, de tels oublis peuvent être très coûteux dans les applications à volume élevé et à interaction élevée.

Amélioration des performances de rendu de React

Alors, compte tenu de tout cela, que pouvons-nous faire pour améliorer les performances de nos applications ? Il s'avère que nous pouvons faire certaines choses, mais notez que nous ne discuterons que dans le contexte des composants fonctionnels. Les composants basés sur les classes sont fortement découragés par l'équipe React et sont en voie de disparition.

Utilisez Redux ou des bibliothèques similaires pour la gestion de l'état

Ceux qui aiment le monde rapide et sale de Context ont tendance à détester Redux, mais cette chose est extrêmement populaire pour de bonnes raisons. Et l'une de ces raisons est la performance - la fonction connect() dans Redux est magique car elle ne rend (presque toujours) correctement que ces composants si nécessaire. Oui, suivez simplement l'architecture Redux standard et les performances sont gratuites. Il n'est pas du tout exagéré que si vous adoptez l'architecture Redux, vous évitez immédiatement la plupart des problèmes de performances (et autres).

Utilisez memo() pour "geler" les composants

Le nom "memo" vient de Memoization, qui est un nom fantaisiste pour la mise en cache. Et si vous n'avez pas beaucoup rencontré la mise en cache, ce n'est pas grave ; voici une description édulcorée : chaque fois que vous avez besoin d'un résultat de calcul/d'opération, vous regardez à l'endroit où vous avez conservé les résultats précédents ; si vous le trouvez, super, renvoyez simplement ce résultat ; sinon, continuez et effectuez cette opération/calcul.

Avant de plonger directement dans memo() , voyons d'abord comment un rendu inutile se produit dans React. Nous commençons par un scénario simple : une infime partie de l'interface utilisateur de l'application qui montre à l'utilisateur combien de fois il a aimé le service/produit (si vous avez du mal à accepter le cas d'utilisation, pensez à comment sur Medium vous pouvez "applaudir ” plusieurs fois pour montrer à quel point vous soutenez/aimez un article).

Il y a aussi un bouton qui leur permet d'augmenter les likes de 1. Et enfin, il y a un autre composant à l'intérieur qui montre aux utilisateurs les détails de base de leur compte. Ne vous inquiétez pas du tout si vous trouvez cela difficile à suivre ; Je vais maintenant fournir un code étape par étape pour tout (et il n'y en a pas beaucoup), et à la fin, un lien vers un terrain de jeu où vous pouvez jouer avec l'application de travail et améliorer votre compréhension.

Abordons d'abord le composant concernant les informations client. Créons un fichier appelé CustomerInfo.js qui contient le code suivant :

 import React from "react"; export const CustomerInfo = () => { console.log("CustomerInfo was rendered! :O"); return ( <React.Fragment> <p>Name: Sam Punia</p> <p>Email: [email protected]</p> <p>Preferred method: Online</p> </React.Fragment> ); };

Rien d'extraordinaire, non ?

Juste un texte informatif (qui aurait pu passer par des accessoires) qui ne devrait pas changer lorsque l'utilisateur interagit avec l'application (pour les puristes, oui, bien sûr, cela peut changer, mais le fait est que, par rapport au reste de l'application, il est pratiquement statique). Mais notez l' console.log() . Ce sera notre indice pour savoir que le composant a été rendu (rappelez-vous, "rendu" signifie que ses informations ont été collectées et calculées/comparées, et non qu'elles ont été peintes sur le DOM réel).

Ainsi, lors de nos tests, si nous ne voyons aucun message de ce type dans la console du navigateur, notre composant n'a pas été rendu du tout ; si nous le voyons apparaître 10 fois, cela signifie que le composant a été rendu 10 fois ; etc.

Et maintenant, voyons comment notre composant principal utilise ce composant d'informations client :

 import React, { useState } from "react"; import "./styles.css"; import { CustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <CustomerInfo /> </div> </div> ); }

Nous voyons donc que le composant App a un état interne géré via le crochet useState() . Cet état continue de compter le nombre de fois que l'utilisateur a aimé le service/site et est initialement défini sur zéro. Rien de difficile en ce qui concerne les applications React, n'est-ce pas ? Du côté de l'interface utilisateur, les choses ressemblent à ceci :

Le bouton a l'air trop tentant pour ne pas être défoncé, du moins pour moi ! Mais avant cela, je vais ouvrir la console de développement de mon navigateur et l'effacer. Après cela, je vais écraser le bouton plusieurs fois, et voici ce que je vois :

J'ai appuyé 19 fois sur le bouton et, comme prévu, le nombre total de likes est de 19. La palette de couleurs rendait la lecture très difficile, j'ai donc ajouté un cadre rouge pour mettre en évidence l'élément principal : le composant <CustomerInfo /> a été rendu 20 fois !

Pourquoi 20 ?

Une fois lorsque tout a été initialement rendu, puis 19 fois lorsque le bouton a été enfoncé. Le bouton change totalLikes , qui est un élément d'état à l'intérieur du composant <App /> et, par conséquent, le composant principal s'affiche à nouveau. Et comme nous l'avons appris dans les sections précédentes de cet article, tous les composants à l'intérieur sont également restitués. Ceci est indésirable car le composant <CustomerInfo /> n'a pas changé dans le processus et a pourtant contribué au processus de rendu.

Comment pouvons-nous empêcher cela?

Exactement comme le titre de cette section l'indique, utilisez la fonction memo() pour créer une copie "conservée" ou mise en cache du composant <CustomerInfo /> . Avec un composant mémorisé, React regarde ses accessoires et les compare aux accessoires précédents, et s'il n'y a pas de changement, React n'extrait pas une nouvelle sortie de "rendu" de ce composant.

Ajoutons cette ligne de code à notre fichier CustomerInfo.js :

 export const MemoizedCustomerInfo = React.memo(CustomerInfo);

Ouais, c'est tout ce qu'on a à faire ! Il est maintenant temps de l'utiliser dans notre composant principal et de voir si quelque chose change :

 import React, { useState } from "react"; import "./styles.css"; import { MemoizedCustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <MemoizedCustomerInfo /> </div> </div> ); }

Oui, seules deux lignes ont changé, mais je voulais quand même montrer le composant entier. Rien n'a changé au niveau de l'interface utilisateur, donc si je prends la nouvelle version pour un tour et que j'écrase le bouton J'aime plusieurs fois, j'obtiens ceci :

Alors, combien de messages de console avons-nous ?

Seulement un! Cela signifie qu'à part le rendu initial, le composant n'a pas du tout été touché. Imaginez les gains de performances sur une application vraiment à grande échelle ! D'accord, d'accord, le lien vers le jeu de code que j'ai promis est ici. Pour répliquer l'exemple précédent, vous devrez importer et utiliser CustomerInfo au lieu de MemoizedCustomerInfo de CustomerInfo.js .

Cela dit, memo() n'est pas du sable magique que vous pouvez saupoudrer partout et attendre des résultats magiques. Une utilisation excessive de memo() peut également introduire des bogues délicats dans votre application et, parfois, entraîner simplement l'échec de certaines mises à jour attendues. Les conseils généraux sur l'optimisation "prématurée" s'appliquent ici aussi. Tout d'abord, créez votre application comme le dit votre intuition ; ensuite, faites un profilage intensif pour voir quelles parties sont lentes et s'il apparaît que les composants mémorisés sont la bonne solution, introduisez-les ensuite.

Conception de composants "intelligents"

J'ai mis « intelligent » entre guillemets parce que : 1) L'intelligence est hautement subjective et situationnelle ; 2) Des actions soi-disant intelligentes ont souvent des conséquences désagréables. Donc, mon conseil pour cette section est : ne sois pas trop confiant dans ce que tu fais.

Avec cela à l'écart, une possibilité d'améliorer les performances de rendu consiste à concevoir et à placer les composants un peu différemment. Par exemple, un composant enfant peut être refactorisé et déplacé quelque part dans la hiérarchie afin d'échapper aux re-rendus. Aucune règle ne dit que "le composant ChatPhotoView doit toujours être à l'intérieur du composant Chat". Dans des cas particuliers (et ce sont des cas où nous avons des preuves étayées par des données que les performances sont affectées), contourner / enfreindre les règles peut en fait être une excellente idée.

Conclusion

Beaucoup plus peut être fait pour optimiser les applications React en général, mais comme cet article concerne le rendu, j'ai limité la portée de la discussion. Quoi qu'il en soit, j'espère que vous avez maintenant une meilleure idée de ce qui se passe dans React sous le capot, de ce qu'est réellement le rendu et de la manière dont il peut affecter les performances de l'application.

Ensuite, comprenons ce qu'est React Hooks ?