Comment identifier et résoudre les rendus perdus dans React

Ainsi, récemment, je pensais au profil de performance d'une application de réaction sur laquelle je travaillais et j'ai soudainement pensé à définir quelques indicateurs de performance. Et j’ai bien compris que la première chose à laquelle je dois remédier est le gaspillage des images que je fais dans chacune des pages Web. Vous pensez peut-être à ce que sont des restes perdus au fait? Laissez-nous plonger dans.

React a depuis le début changé la philosophie de la création d'applications Web et, par la suite, la façon de penser des développeurs front-end. Avec l'introduction de Virtual DOM, React rend les mises à jour d'interface utilisateur aussi efficaces que possible. Cela rend l'expérience de l'application Web soignée. Vous êtes-vous déjà demandé comment rendre vos applications React plus rapides? Pourquoi les applications Web React de taille moyenne ont-elles encore tendance à donner de mauvais résultats? Le problème réside dans la manière dont nous utilisons réellement React!

Comment fonctionne React

Une bibliothèque front-end moderne telle que React ne permet pas à notre application d’accélérer plus rapidement. Premièrement, nous, les développeurs, devrions comprendre le fonctionnement de React. Comment les composants vivent-ils pendant les cycles de vie des composants dans la vie des applications? Donc, avant de plonger dans une technique d'optimisation, nous devons mieux comprendre le fonctionnement réel de React.

Au cœur de React, nous avons la syntaxe JSX et la puissante capacité de React à créer et à comparer des DOM virtuels. Depuis sa publication, React a influencé de nombreuses autres bibliothèques front-end. Par exemple, Vue.js s'appuie également sur l'idée de DOM virtuels.

Chaque application React commence par un composant racine. Nous pouvons considérer l’ensemble de l’application comme une formation arborescente où chaque nœud est un composant. Les composants de React sont des ‘fonctions’ qui rendent l’UI basée sur les données. Cela signifie que les accessoires et l’état qu’il reçoit; dire que c'est CF

UI = CF (données)

Les utilisateurs interagissent avec l'interface utilisateur et provoquent la modification des données. Les interactions sont tout ce qu'un utilisateur peut faire dans notre application. Par exemple, cliquez sur un bouton, faites glisser des images, faites glisser les éléments de la liste et les demandes AJAX appelant des API. Toutes ces interactions ne modifient que les données. Ils ne provoquent jamais de changement dans l'interface utilisateur.

Ici, les données sont tout ce qui définit l'état d'une application. Pas seulement ce que nous avons stocké dans notre base de données. Même différents états frontaux, tels que l'onglet actuellement sélectionné ou le fait qu'une case soit cochée ou non, font partie de ces données. En cas de modification des données, React utilise les fonctions de composant pour restituer l'interface utilisateur, mais uniquement:

UI1 = CF (data1)
UI2 = CF (data2)

React calcule les différences entre l'interface utilisateur actuelle et la nouvelle interface utilisateur en appliquant un algorithme de comparaison sur les deux versions de son DOM virtuel.

Changements = Différence (UI1, UI2)

React applique ensuite uniquement les modifications de l'interface utilisateur à la véritable interface utilisateur du navigateur. Lorsque les données associées à un composant changent, React détermine si une mise à jour réelle du DOM est requise. Cela permet à React d'éviter des opérations de manipulation du DOM potentiellement coûteuses dans le navigateur. Des exemples tels que la création de nœuds DOM et l’accès insoutenable à des nœuds existants.

Cette différenciation et ce rendu répétés des composants peuvent constituer l'une des principales sources de problèmes de performances React dans toutes les applications React. Construire une application React dans laquelle l'algorithme de différenciation ne parvient pas à se réconcilier de manière efficace, entraînant le rendu répété de l'application entière, ce qui provoque des rendus inutiles et peut entraîner une expérience frustrante et lente.

Au cours du processus de rendu initial, React construit un arbre DOM comme ceci -

Supposons qu'une partie des données change. Ce que nous voulons que React fasse, c'est de ne rendre que les composants directement affectés par ce changement spécifique. Ignorer éventuellement le processus de différenciation pour le reste des composants. Supposons que certaines données changent dans la composante 2 de l'image ci-dessus et que ces données ont été passées de R à B, puis à 2. Si R rend de nouveau le rendu, il restituera chacun de ses enfants, ce qui signifie A, B, C. , D et par ce processus, React fait ceci:

Dans l'image ci-dessus, tous les nœuds jaunes sont rendus et différenciés. Cela entraîne une perte de temps / ressources de calcul. C’est là que nous allons principalement mettre nos efforts d’optimisation. Configurer chaque composant pour qu'il ne rende et ne différencie que lorsque c'est nécessaire. Cela nous permettra de récupérer ces cycles de processeur gaspillés. Dans un premier temps, nous examinerons la manière dont nous pouvons identifier les restes perdus de notre application.

Identifier les rendus perdus

Il y a plusieurs façons de le faire. La méthode la plus simple consiste à activer l'option de mise à jour en surbrillance dans les préférences des outils de développement de React.

Lors de l'interaction avec l'application, les mises à jour sont mises en surbrillance à l'écran par des bordures colorées. Par ce processus, vous devriez voir les composants qui ont été rendus de nouveau. Cela nous permet de repérer les rendus qui n'étaient pas nécessaires.

Suivons cet exemple.

Notez que lorsque nous entrons dans une seconde tâche, le premier ‘todo’ clignote également à l’écran à chaque frappe. Cela signifie qu'il est restitué par React avec l'entrée. C'est ce que nous appelons un rendu «gaspillé». Nous savons que cela n’est pas nécessaire car le premier contenu n’a pas changé, mais React ne le sait pas.

Bien que React ne mette à jour que les nœuds DOM modifiés, le rendu du rendu prend encore un certain temps. Dans de nombreux cas, ce n’est pas un problème, mais si le ralentissement est perceptible, nous devrions envisager quelques mesures pour mettre fin à ces rendus redondants.

Utilisation de la méthode shouldComponentUpdate

Par défaut, React restituera le DOM virtuel et comparera la différence entre chaque composant de l’arbre pour toute modification de ses accessoires ou de son état. Mais ce n'est évidemment pas raisonnable. Au fur et à mesure que notre application grandit, essayer à nouveau de restituer et de comparer le DOM virtuel entier à chaque action ralentira le processus.

React fournit une méthode de cycle de vie simple pour indiquer si un composant doit être restitué, c'est-à-dire shouldComponentUpdate, qui est déclenché avant le démarrage du processus de restitution. L'implémentation par défaut de cette fonction renvoie true.

Lorsque cette fonction renvoie la valeur true pour un composant, le processus de différenciation de rendu peut être déclenché. Cela nous donne le pouvoir de contrôler le processus de différenciation du rendu. Supposons que nous devons empêcher un composant d'être restitué, nous devons simplement renvoyer false à partir de cette fonction. Comme nous pouvons le constater grâce à la mise en œuvre de la méthode, nous pouvons comparer les accessoires actuels et suivants ainsi que l’état pour déterminer s’il est nécessaire de procéder à un nouveau rendu:

Utiliser des composants purs

Pendant que vous travaillez sur React, vous connaissez certainement React.Component, mais quel est le problème avec React.PureComponent? Nous avons déjà discuté de la méthode de cycle de vie shouldComponentUpdate, dans les composants purs, il existe déjà une implémentation par défaut de, shouldComponentUpdate () avec une comparaison peu profonde entre prop et state. Ainsi, un composant pur est un composant qui ne restitue que si props / state est différent des précédents props et state.

Dans la comparaison superficielle, les types de données primitifs tels que chaîne, booléen, nombre sont comparés par valeur et les types de données complexes comme tableau, objet, fonction sont comparés par référence

Mais que se passe-t-il si nous avons un composant fonctionnel sans état dans lequel nous devons implémenter cette méthode de comparaison avant chaque nouvelle restitution? React a un composant d'ordre supérieur React.memo. C'est comme React.PureComponent mais pour les composants fonctionnels au lieu des classes.

Par défaut, il fait la même chose que shouldComponentUpdate (), qui ne compare que superficiellement l'objet props. Mais si nous voulons avoir le contrôle de cette comparaison? Nous pouvons également fournir une fonction de comparaison personnalisée comme second argument.

Rendre les données immuables

Et si nous pouvions utiliser un React.PureComponent tout en ayant un moyen efficace de dire quand des propriétés ou des états complexes comme un tableau, un objet, etc. ont changé automatiquement? C’est là que la structure de données immuable facilite la vie.

L'idée d'utiliser des structures de données immuables est simple. Comme nous l'avons vu précédemment, pour les types de données complexes, la comparaison est effectuée par rapport à leur référence. Chaque fois qu'un objet contenant des données complexes change, au lieu d'effectuer les modifications dans cet objet, nous pouvons créer une copie de cet objet avec les modifications qui créeront une nouvelle référence.

ES6 a un opérateur de propagation d'objet pour y arriver.

Nous pouvons faire la même chose pour les tableaux:

Évitez de transmettre une nouvelle référence pour les mêmes anciennes données

Nous savons que chaque fois que les accessoires d’un composant changent, un rendu est effectué. Mais parfois, les accessoires ne changent pas. Nous écrivons du code de manière que React pense qu’il a changé, ce qui entraînera également un nouveau rendu, mais cette fois-ci un rendu inutile. Donc, en gros, nous devons nous assurer que nous transmettons une référence différente comme support pour différentes données. En outre, nous devons éviter de transmettre une nouvelle référence pour les mêmes données. Nous allons maintenant examiner certains cas où nous créons ce problème. Regardons ce code.

Voici le contenu du composant BookInfo où nous rendons deux composants, BookDescription et BookReview. Ceci est le code correct, et cela fonctionne bien, mais il y a un problème. BookDescription ré-rendra chaque fois que nous aurons de nouvelles critiques de données comme accessoires. Pourquoi? Dès que le composant BookInfo reçoit de nouveaux accessoires, la fonction de rendu est appelée pour créer son arborescence d'éléments. La fonction de rendu crée une nouvelle constante de livre qui signifie qu'une nouvelle référence est créée. BookDescription aura donc ce livre comme référence de nouvelles, ce qui provoquera le rendu de BookDescription. Donc, nous pouvons reformuler ce morceau de code en ceci:

Maintenant, la référence est toujours la même, this.book et un nouvel objet ne sont pas créés au moment du rendu. Cette philosophie de re-rendu s’applique à tous les accessoires, y compris les gestionnaires d’événements, tels que:

Ici, nous avons utilisé deux méthodes différentes (méthodes de liaison et utilisation de la fonction de flèche dans le rendu) pour appeler les méthodes de gestionnaire d’événements, mais les deux créeront une nouvelle fonction à chaque fois que le composant effectuera un nouveau rendu. Donc, pour résoudre ces problèmes, nous pouvons lier la méthode au constructeur et utiliser des propriétés de classe qui sont encore à expérimenter et non encore normalisées, mais de nombreux développeurs utilisent déjà cette méthode pour transmettre des fonctions à d'autres composants dans des applications prêtes à la production:

Emballer

En interne, React utilise plusieurs techniques intelligentes pour minimiser le nombre d'opérations DOM coûteuses nécessaires à la mise à jour de l'interface utilisateur. Pour de nombreuses applications, l’utilisation de React aboutit à une interface utilisateur rapide sans beaucoup de travail d’optimisation des performances. Néanmoins, si nous pouvons suivre les techniques que j'ai mentionnées ci-dessus pour résoudre les rendus perdus, nous obtiendrons également une expérience très fluide en termes de performances pour les applications volumineuses.