Amélioration de la mise en cache des gestionnaires d'événements et des performances des applications React

Aujourd'hui, nous publions une traduction du matériel, dont l'auteur, après avoir analysé les caractéristiques du travail avec des objets en JavaScript, propose aux développeurs de React une méthodologie pour accélérer les applications. En particulier, nous parlons du fait qu'une variable, qui, comme on dit, est "affectée à un objet", et qui est souvent appelée simplement "objet", en fait, ne stocke pas l'objet lui-même, mais un lien vers celui-ci. Les fonctions en JavaScript sont également des objets, donc ce qui précède est vrai pour eux. Gardant cela à l'esprit, la conception de composants React et l'analyse critique de leur code peuvent améliorer leurs mécanismes internes et améliorer les performances des applications.



Fonctionnalités de travail avec des objets en JavaScript


Si vous créez quelques fonctions qui se ressemblent exactement et que vous essayez de les comparer, il s'avère qu'elles sont différentes du point de vue du système. Afin de vérifier cela, vous pouvez exécuter le code suivant:

const functionOne = function() { alert('Hello world!'); }; const functionTwo = function() { alert('Hello world!'); }; functionOne === functionTwo; // false 

Essayons maintenant d'affecter une variable à une fonction existante déjà affectée à une autre variable et comparons ces deux variables:

 const functionThree = function() { alert('Hello world!'); }; const functionFour = functionThree; functionThree === functionFour; // true 

Comme vous pouvez le voir, avec cette approche, l'opérateur d'égalité stricte renvoie true .
Les objets se comportent naturellement de la même manière:

 const object1 = {}; const object2 = {}; const object3 = object1; object1 === object2; // false object1 === object3; // true 

Ici, nous parlons de JavaScript, mais si vous avez de l'expérience dans le développement dans d'autres langues, vous connaissez peut-être le concept de pointeurs. Dans le code ci-dessus, chaque fois qu'un objet est créé, une partie de la mémoire système lui est allouée. Lorsque nous utilisons une commande de la forme object1 = {} , cela conduit à remplir avec certaines données un morceau de mémoire alloué spécifiquement pour object1 .

Il est tout à fait possible d'imaginer object1 comme l'adresse à laquelle les structures de données liées à l'objet sont situées en mémoire. L'exécution de la commande object2 = {} conduit à l'allocation d'une autre zone mémoire, conçue spécifiquement pour object2 . obect1 et obect1 sont- obect1 object2 dans la même zone de mémoire? Non, chacun d'eux a son propre complot. C'est pourquoi lorsque nous essayons de comparer object1 et object1 object2 nous obtenons false . Ces objets peuvent avoir une structure identique, mais les adresses dans la mémoire où ils se trouvent diffèrent, et ce sont les adresses qui sont vérifiées lors de la comparaison.

En exécutant la commande object3 = object1 , nous écrivons l'adresse de object1 dans la constante object3 . Ce n'est pas un nouvel objet. Cette constante se voit attribuer l'adresse d'un objet existant. Vous pouvez le vérifier en:

 const object1 = { x: true }; const object3 = object1; object3.x = false; object1.x; // false 

Dans cet exemple, un objet est créé en mémoire et son adresse est écrite dans la constante object1 . Ensuite, la même adresse est écrite dans la constante object3 . Changer object3 change l'objet en mémoire. Cela signifie que lorsque vous accédez à un objet en utilisant toute autre référence à celui-ci, par exemple celui stocké dans object1 , nous travaillerons déjà avec sa version modifiée.

Fonctions, objets et réaction


La méconnaissance du mécanisme ci-dessus par les développeurs novices conduit souvent à des erreurs et, peut-être, la prise en compte des fonctionnalités de travail avec des objets mérite un article séparé. Cependant, notre sujet aujourd'hui est la performance des applications React. Dans ce domaine, des erreurs peuvent être commises même par des développeurs assez expérimentés qui ne font tout simplement pas attention à la façon dont les applications React sont affectées par le fait que les variables et les constantes JavaScript ne sont pas stockées dans les objets eux-mêmes, mais seulement des liens vers eux.

Qu'est-ce que cela a à voir avec React? React dispose de mécanismes intelligents pour économiser les ressources système visant à améliorer les performances des applications: si les propriétés et l'état du composant ne changent pas, alors ce que la fonction de render ne change pas. Évidemment, si le composant reste le même, il n'a pas besoin d'être rendu à nouveau. Si rien ne change, la fonction de render retournera la même chose qu'avant, il n'est donc pas nécessaire de l'exécuter. Ce mécanisme rend React rapide. Quelque chose ne s'affiche que lorsque cela est nécessaire.

React vérifie l'égalité des propriétés et de l'état des composants à l'aide de fonctionnalités JavaScript standard, c'est-à-dire qu'il les compare simplement à l'aide de l'opérateur == . React n'effectue pas de comparaison «superficielle» ou «profonde» d'objets afin de déterminer leur égalité. Une «comparaison superficielle» est un concept utilisé pour décrire une comparaison de chaque paire clé-valeur d'un objet, par opposition à une comparaison dans laquelle seules les adresses des objets en mémoire sont comparées (références à celles-ci). La comparaison «profonde» des objets va encore plus loin, et si les valeurs des propriétés comparées des objets sont également des objets, elles comparent également les paires clé-valeur de ces objets. Ce processus est répété pour tous les objets imbriqués dans d'autres objets. React ne fait rien de tel, vérifiant simplement l'égalité des liens.

Si, par exemple, vous modifiez la propriété d'un composant représenté par un objet de la forme { x: 1 } en un autre objet qui a exactement la même apparence, React restitue le composant, car ces objets se trouvent dans différentes zones de mémoire. Si vous vous souvenez de l'exemple ci-dessus, alors, lorsque vous changez les propriétés d'un composant d' object1 en object3 , React ne restitue pas un tel composant, car les constantes object1 et object3 font référence au même objet.

L'utilisation des fonctions en JavaScript est organisée exactement de la même manière. Si React rencontre les mêmes fonctionnalités dont les adresses sont différentes, il sera restitué. Si la «nouvelle fonction» n'est qu'un lien vers une fonction qui a déjà été utilisée, il n'y aura pas de nouveau rendu.

Un problème typique lors de l'utilisation de composants


Voici l'un des scénarios de travail avec des composants, qui, malheureusement, me revient constamment à l'esprit lors de la vérification du code de quelqu'un d'autre:

 class SomeComponent extends React.PureComponent { get instructions() {   if (this.props.do) {     return 'Click the button: ';   }   return 'Do NOT click the button: '; } render() {   return (     <div>       {this.instructions}       <Button onClick={() => alert('!')} />     </div>   ); } } 

Devant nous est un composant très simple. Il s'agit d'un bouton, lorsque vous cliquez dessus, une notification s'affiche. À côté du bouton sont affichées des instructions pour son utilisation, informant l'utilisateur s'il doit appuyer sur ce bouton. Contrôlez quelle instruction sera affichée en définissant la SomeComponent do ( do={true} ou do={false} ) SomeComponent .

Chaque fois que le composant SomeComponent est restitué (lorsque la valeur de la propriété do passe de true à false et vice versa), l'élément Button est également rendu. Le gestionnaire onClick , bien qu'il soit toujours le même, est recréé à chaque appel de la fonction de render . Par conséquent, il s'avère que chaque fois que le composant est affiché en mémoire, une nouvelle fonction est créée, puisque sa création est effectuée dans la fonction de render , un lien vers la nouvelle adresse en mémoire est transmis à <Button /> , et le composant Button est également rendu à nouveau, malgré le fait que dans rien n'a changé du tout.

Parlons de la façon de le réparer.

Résolution de problèmes


Si la fonction est indépendante du composant ( this contexte), vous pouvez la définir en dehors du composant. Toutes les instances du composant utiliseront la même référence de fonction, car dans tous les cas, ce sera la même fonction. Voici à quoi ça ressemble:

 const createAlertBox = () => alert('!'); class SomeComponent extends React.PureComponent { get instructions() {   if (this.props.do) {     return 'Click the button: ';   }   return 'Do NOT click the button: '; } render() {   return (     <div>       {this.instructions}       <Button onClick={createAlertBox} />     </div>   ); } } 

Contrairement à l'exemple précédent, createAlertBox , avec chaque appel à render , contiendra le même lien vers la même zone en mémoire. Par conséquent, Button sortie répétée Button ne sera pas exécutée.

Alors que le composant Button est petit et rendu rapidement, le problème décrit ci-dessus associé à la déclaration interne des fonctions peut également être trouvé dans les grands composants complexes qui prennent beaucoup de temps à rendre. Cela peut ralentir considérablement l'application React. À cet égard, il est logique de suivre la recommandation, selon laquelle de telles fonctions ne devraient jamais être déclarées dans la méthode de render .

Si la fonction dépend du composant, c'est-à-dire qu'elle ne peut pas être définie en dehors de celui-ci, la méthode du composant peut être passée en tant que gestionnaire d'événements:

 class SomeComponent extends React.PureComponent { createAlertBox = () => {   alert(this.props.message); }; get instructions() {   if (this.props.do) {     return 'Click the button: ';   }   return 'Do NOT click the button: '; } render() {   return (     <div>       {this.instructions}       <Button onClick={this.createAlertBox} />     </div>   ); } } 

Dans ce cas, dans chaque instance de SomeComponent lorsque vous cliquez sur le bouton, divers messages s'affichent. Le gestionnaire d'événements de l'élément Button doit être unique à SomeComponent . Lors du passage de la méthode cteateAlertBox , peu importe si SomeComponent restitué. Peu importe si la propriété du message a changé. L'adresse de la fonction createAlertBox change pas, ce qui signifie que l'élément Button ne doit pas être restitué. Grâce à cela, vous pouvez économiser des ressources système et améliorer la vitesse de rendu de l'application.

Tout cela est bien. Mais que faire si les fonctions sont dynamiques?

Résoudre un problème plus complexe


L'auteur de ce document vous demande de prêter attention au fait qu'il a préparé les exemples de cette section, en prenant la première chose qui lui est venue à l'esprit, appropriée pour illustrer la réutilisation des fonctions. Ces exemples sont destinés à aider le lecteur à saisir l'essence de l'idée. Bien que cette section soit recommandée à la lecture pour comprendre l'essence de ce qui se passe, l'auteur conseille de prêter attention aux commentaires sur l' article original , car certains lecteurs ont suggéré de meilleures versions des mécanismes discutés ici, qui prennent en compte les fonctionnalités d'invalidation du cache et les mécanismes de gestion de la mémoire intégrés dans React.

Ainsi, il est extrêmement courant que dans un composant, il existe de nombreux gestionnaires d'événements dynamiques uniques, par exemple, quelque chose de similaire peut être vu dans le code, où la méthode de tableau de map est utilisée dans la méthode de render :

 class SomeComponent extends React.PureComponent { render() {   return (     <ul>       {this.props.list.map(listItem =>         <li key={listItem.text}>           <Button onClick={() => alert(listItem.text)} />         </li>       )}     </ul>   ); } } 

Ici, un nombre différent de boutons sera affiché et un nombre différent de gestionnaires d'événements seront créés, chacun étant représenté par une fonction unique, et, à l'avance, lors de la création de SomeComponent , on ne sait pas quelles seront ces fonctions. Comment résoudre ce puzzle?

Ici, la mémorisation nous aidera, ou, plus simplement, la mise en cache. Pour chaque valeur unique, créez une fonction et placez-la dans le cache. Si cette valeur unique se reproduit, il suffira de prendre dans le cache la fonction qui lui correspond, qui était auparavant placée dans le cache.

Voici à quoi ressemble la mise en œuvre de cette idée:

 class SomeComponent extends React.PureComponent { //    SomeComponent        //   . clickHandlers = {}; //       //    . getClickHandler(key) {   //       ,  .   if (!Object.prototype.hasOwnProperty.call(this.clickHandlers, key)) {     this.clickHandlers[key] = () => alert(key);   }   return this.clickHandlers[key]; } render() {   return (     <ul>       {this.props.list.map(listItem =>         <li key={listItem.text}>           <Button onClick={this.getClickHandler(listItem.text)} />         </li>       )}     </ul>   ); } } 

Chaque élément du tableau est traité par la méthode getClickHandler . Cette méthode, la première fois qu'elle sera appelée avec une certaine valeur, créera une fonction unique à cette valeur, la placera dans le cache et la renverra. Tous les appels ultérieurs à cette méthode, en lui passant la même valeur, la feront simplement renvoyer un lien vers la fonction à partir du cache.

Par conséquent, le nouveau rendu de SomeComponent ne rendra pas le Button . De même, l'ajout d'éléments à la propriété list créera dynamiquement des gestionnaires d'événements pour chaque bouton.

Vous devrez être créatif dans la création d'identifiants uniques pour les gestionnaires s'ils sont définis par plusieurs variables, mais ce n'est pas beaucoup plus compliqué que la création habituelle d'une propriété de key unique pour chaque objet JSX obtenu à la suite de la méthode map .

Ici, je voudrais vous mettre en garde contre d'éventuels problèmes d'utilisation d'index de tableau comme identificateurs. Le fait est qu'avec cette approche, vous pouvez rencontrer des erreurs si l'ordre des éléments du tableau change ou si certains de ses éléments sont supprimés. Ainsi, par exemple, si au début un tableau similaire ressemblait à [ 'soda', 'pizza' ] , puis transformé en [ 'pizza' ] , et que vous mettiez en cache les gestionnaires d'événements à l'aide d'une commande de la forme listeners[0] = () => alert('soda') , vous constaterez que lorsque l'utilisateur clique sur le bouton auquel le gestionnaire avec l'identifiant 0 est affecté et qui, conformément au contenu du tableau [ 'pizza' ] , devrait afficher un message de pizza , un message de soda sera affiché. Pour la même raison, il n'est pas recommandé d'utiliser des indices de tableau comme propriétés de clé.

Résumé


Dans cet article, nous avons examiné les fonctionnalités des mécanismes internes JavaScript, en envisageant la possibilité d'accélérer le rendu des applications React. Nous espérons que les idées présentées ici vous seront utiles.

Chers lecteurs! Si vous connaissez des moyens intéressants d'optimiser les applications React, partagez-les.

Source: https://habr.com/ru/post/fr426053/


All Articles