Le prix caché des bibliothèques CSS-in-JS dans les applications React

Dans les applications frontales modernes, la technologie CSS-in-JS est très populaire. Le fait est que cela donne aux développeurs un mécanisme pour travailler avec des styles qui est plus pratique que le CSS ordinaire. Ne vous méprenez pas. J'aime vraiment CSS, mais créer une bonne architecture CSS n'est pas une tâche facile. La technologie CSS-in-JS présente des avantages importants par rapport aux styles CSS conventionnels. Mais, malheureusement, l'utilisation de CSS-in-JS peut, dans certaines applications, entraîner des problèmes de performances. Dans cet article, je vais essayer d'analyser les fonctionnalités de haut niveau des bibliothèques CSS-in-JS les plus populaires, de parler de certains des problèmes qui surviennent parfois lors de leur utilisation et de suggérer des moyens d'atténuer ces problèmes.



Aperçu de la situation


Dans mon entreprise, il a été décidé de créer une bibliothèque d'interface utilisateur. Cela nous apporterait un avantage considérable, nous permettrait de réutiliser des fragments standard d'interfaces dans divers projets. J'étais l'un des volontaires qui ont pris la relève. J'ai décidé d'utiliser la technologie CSS-in-JS, car je connaissais déjà l'API de style des bibliothèques CSS-in-JS les plus populaires. Au cours du travail, je me suis efforcé d'agir raisonnablement. J'ai conçu une logique réutilisable et appliqué des propriétés partagées dans les composants. J'ai donc repris la composition des composants. Par exemple, le <IconButton /> étendu le composant <BaseButton /> , qui, à son tour, était une implémentation d'une entité styled.button simple. Malheureusement, il s'est avéré que IconButton besoin de son propre style, j'ai donc converti ce composant en composant stylisé:

 const IconButton = styled(BaseButton)`  border-radius: 3px; `; 

Comme de plus en plus de composants sont apparus dans notre bibliothèque, nous avons utilisé de plus en plus d'opérations de composition. Cela ne semblait pas contre nature. Après tout, la composition est, en fait, le fondement de React. Tout allait bien jusqu'à ce que je crée le composant Table . J'ai commencé à sentir que cette composante se rendait lentement. Surtout dans les situations où le nombre de lignes dans le tableau dépasse 50. C'était incorrect. Par conséquent, j'ai commencé à comprendre le problème en recourant aux outils de développement.

Soit dit en passant, si vous vous êtes déjà demandé pourquoi les règles CSS ne peuvent pas être modifiées à l'aide de l'inspecteur d'outils de développement, sachez que c'est parce qu'elles utilisent CSSStyleSheet.insertRule () . Il s'agit d'un moyen très rapide de modifier les feuilles de style. Mais l'un de ses inconvénients est le fait que les feuilles de style correspondantes ne peuvent plus être éditées par l'inspecteur.

Inutile de dire que l'arbre généré par React était vraiment énorme. Le nombre de composants Context.Consumer était si grand qu'il pouvait me priver de sommeil. Le fait est que chaque fois qu'un composant stylisé unique est rendu à l'aide de composants Context.Consumer ou d' émotion , en plus de créer un composant React normal, un composant Context.Consumer supplémentaire est également Context.Consumer . Cela est nécessaire pour permettre au script correspondant (la plupart des bibliothèques CSS-in-JS dépendent des scripts exécutés pendant que la page est en cours d'exécution) de traiter correctement les règles de style générées. Habituellement, cela ne pose aucun problème particulier, mais nous ne devons pas oublier que les composants doivent avoir accès au sujet. Cela se traduit par la nécessité de rendre Context.Consumer supplémentaire pour chaque élément stylisé, ce qui vous permet de "lire" le thème à partir ThemeProvider composant ThemeProvider . Par conséquent, lors de la création d'un composant stylisé dans une application avec un thème, 3 composants sont créés. Il s'agit d'un composant StyledXXX et de deux autres composants Context.Consumer .

Certes, il n'y a rien de particulièrement terrible ici, car React fait son travail rapidement, ce qui signifie que dans la plupart des cas, nous n'avons rien à craindre. Mais que se passe-t-il si plusieurs composants stylisés sont assemblés afin de créer un composant plus complexe? Que faire si ce composant complexe fait partie d'une longue liste ou d'un grand tableau où au moins 100 de ces composants sont rendus? Ici, dans de telles situations, nous sommes confrontés à des problèmes ...

Profilage


Afin de tester différentes solutions CSS-in-JS, j'ai créé une application simple. Il affiche 50 fois le texte Hello World . Dans la première version de cette application, j'ai enveloppé ce texte dans un élément div ordinaire. Dans le second , j'ai utilisé le composant styled.div . De plus, j'ai ajouté un bouton à l'application qui provoque le rendu de tous ces 50 éléments.

Après avoir rendu le composant <App /> , deux arborescences React différentes ont été affichées. Les figures suivantes montrent les arbres d'éléments déduits par React.


Un arbre affiché dans une application qui utilise un élément div normal


Une arborescence affichée dans une application qui utilise styled.div

Ensuite, en utilisant le bouton, j'ai rendu <App /> 10 fois afin de collecter des données concernant la charge sur le système, que les composants supplémentaires de Context.Consumer . Voici des informations sur le re-rendu répétitif d'une application avec des éléments div réguliers en mode conception.


Re-rendu de l'application avec des éléments div réguliers en mode conception. La valeur moyenne est de 2,54 ms.


Re-rendu de l'application avec des éléments styled.div en mode développement. La valeur moyenne est de 3,98 ms.

Ce qui est très intéressant, c'est qu'en moyenne, une application CSS-in-JS est 56,6% plus lente que d'habitude. Mais c'était un mode de développement. Et le mode de production?


Re-rendu de l'application avec les éléments div habituels en mode production. La valeur moyenne est de 1,06 ms.


Re-rendu de l'application avec des éléments styled.div en mode production. La valeur moyenne est de 2,27 ms.

Lorsque le mode production est activé, l'implémentation div de l'application semble être plus de 50% plus rapide par rapport à la même version en mode développement. Une application styled.div n'est que 43% plus rapide. Et ici, comme précédemment, il est clair que la solution CSS-in-JS est presque deux fois plus lente que la solution habituelle. Qu'est-ce qui le ralentit?

Analyse de l'application lors de son exécution


La réponse évidente à la question de ce qui ralentit une application CSS-in-JS peut être la suivante: "Il a été dit qu'une centaine de bibliothèques CSS-in-JS rendent deux Context.Consumer par composant." Mais si vous pensez à tout cela Context.Consumer , alors Context.Consumer n'est qu'un mécanisme pour accéder à une variable JS. Bien sûr, React doit faire un certain travail afin de savoir où lire la valeur correspondante, mais cela seul n'explique pas les résultats de mesure ci-dessus. La vraie réponse à cette question peut être trouvée en analysant la raison d'utiliser Context.Consumer . Le fait est que la plupart des bibliothèques CSS-in-JS s'appuient sur des scripts qui s'exécutent pendant la sortie de page dans le navigateur, ce qui aide les bibliothèques à mettre à jour dynamiquement les styles de composants. Ces bibliothèques ne créent pas de classes CSS lors de l'assemblage de pages. Au lieu de cela, ils génèrent et mettent à jour dynamiquement les balises <style> dans le document. Cela se fait lorsque les composants sont montés ou lorsque leurs propriétés changent. Ces balises contiennent généralement une seule classe CSS dont le nom haché correspond à un seul composant React. Lorsque les propriétés de ce composant changent, la <style> correspondante doit également changer. Voici comment décrire ce qui se fait pendant ce processus:

  • Les règles CSS que la <style> doit avoir sont régénérées.
  • Un nouveau nom de classe hachée est créé qui est utilisé pour stocker les règles CSS susmentionnées.
  • La propriété classname du composant React correspondant est mise à jour vers une nouvelle, indiquant la classe qui vient d'être créée.

Considérez, par exemple, la bibliothèque de styled-components . Lors de la création du composant styled.div bibliothèque attribue un identifiant interne (ID) à ce composant et ajoute une nouvelle <style> à la <head> HTML <head> . Cette balise contient un seul commentaire qui fait référence à l'identifiant interne du composant React auquel appartient le style correspondant:

 <style data-styled-components>   /* sc-component-id: sc-bdVaJa */ </style> 

Et voici les actions que la bibliothèque de composants de style effectue lors de l'affichage du composant React correspondant:

  1. Analyse les règles CSS à partir de la chaîne de modèle des composants de style.
  2. Génère un nouveau nom de classe CSS (ou découvre s'il faut conserver le nom précédent).
  3. Effectue le prétraitement des styles à l'aide de stylets.
  4. Incorpore le CSS résultant du prétraitement dans la <style> correspondante de la balise HTML <head> .

Pour pouvoir utiliser la rubrique à l'étape 1 de ce processus, Context.Consumer est requis . Grâce à ce composant, les valeurs sont lues à partir du sujet dans la chaîne de modèle. Afin de pouvoir modifier la <style> associée à ce composant, un autre Context.Consumer nécessaire à partir du composant. Il vous permet d'accéder à une instance d'une feuille de style. C'est pourquoi dans la plupart des bibliothèques CSS-in-JS, nous rencontrons deux instances de Context.Consumer .

De plus, étant donné que tous ces calculs affectent l'interface utilisateur, il convient de noter qu'ils doivent être effectués lors de la phase de rendu des composants. Ils ne peuvent pas être exécutés dans le code des gestionnaires d'événements pour le cycle de vie des composants React (de cette façon, leur exécution peut être retardée et ressemblera à une formation de page lente pour l'utilisateur). C'est pourquoi le rendu des applications styled.div est plus lent que le rendu d'une application standard.

Tout cela a été remarqué par les développeurs de composants de style. Ils ont optimisé la bibliothèque afin de réduire le temps de re-rendu des composants. En particulier, la bibliothèque vérifie si le composant stylisé est «statique». Autrement dit, que les styles du composant dépendent du thème ou des propriétés qui lui sont transmises. Par exemple, le composant statique est illustré ci-dessous:

 const StaticStyledDiv = styled.div`  color:red `; 

Et ce composant n'est pas statique:

 const DynamicStyledDiv = styled.div`  color: ${props => props.color} `; 

Si la bibliothèque découvre que le composant est statique, elle saute les 4 étapes ci-dessus, se rendant compte qu'elle n'aura jamais à changer le nom de classe généré (car il n'y a pas d'élément dynamique pour lequel il peut être nécessaire de modifier les règles CSS qui lui sont associées). De plus, dans cette situation, la bibliothèque n'affiche pas ThemeContext.Consumer autour du composant stylisé, car la dépendance du thème ne permettrait plus au composant d'être considéré comme "statique".

Si vous avez été suffisamment prudent lors de l'analyse des captures d'écran présentées précédemment, vous remarquerez peut-être que même en mode production pour chaque styled.div deux composants Context.Consumer sont Context.Consumer . Chose intéressante, le composant rendu était «statique», car aucune règle CSS dynamique ne lui était associée. Dans une telle situation, on pourrait s'attendre à ce que si cet exemple était écrit à l'aide de la bibliothèque de composants de style, il Context.Consumer pas Context.Consumer nécessaire pour travailler avec le thème. La raison pour laquelle exactement deux Context.Consumer sont affichés ici est que l'expérience, dont les données sont données ci-dessus, a été réalisée en utilisant l'émotion - une autre bibliothèque CSS-in-JS. Cette bibliothèque adopte presque la même approche que les composants de style. Les différences entre eux sont minimes. Ainsi, la bibliothèque d'émotions analyse la chaîne de modèle, prétraite les styles à l'aide de stylets et met à jour le contenu de la <style> correspondante. Ici, cependant, une différence clé doit être notée entre les composants de style et l'émotion. Elle consiste dans le fait que la bibliothèque d'émotions ThemeContext.Consumer toujours tous les composants de ThemeContext.Consumer - qu'ils utilisent ou non le thème (cela explique l'apparence de la capture d'écran ci-dessus). Chose intéressante, même si l'émotion rend plus de composants de consommation que de composants de style, les émotions surpassent les composants de style en termes de performances. Cela indique que le nombre de composants Context.Consumer n'est pas un facteur majeur de ralentissement du rendu. Il convient de noter qu'au moment de la rédaction de ce document, une version bêta des composants de style v5.xx a été publiée, ce qui, selon les développeurs de la bibliothèque, contourne l' émotion en termes de performances.

Résumez de quoi nous parlions. Il s'avère qu'une combinaison de nombreux éléments Context.Consumer (ce qui signifie que React doit coordonner le travail des éléments supplémentaires) et des mécanismes de style dynamique internes peuvent ralentir l'application. Je dois dire que toutes les balises <style> ajoutées à <head> pour chaque composant ne sont jamais supprimées. Cela est dû au fait que les opérations de suppression d'éléments créent une charge importante sur le DOM (par exemple, le navigateur doit réorganiser la page à cause de cela). Cette charge est supérieure à la charge supplémentaire sur le système provoquée par la présence sur la page d'éléments <style> inutiles. Pour être honnête, je ne peux pas affirmer avec certitude que les balises <style> inutiles peuvent entraîner des problèmes de performances. Ils stockent simplement les classes inutilisées générées pendant le fonctionnement de la page (c'est-à-dire que ces données n'ont pas été transmises sur le réseau). Mais vous devez connaître cette fonctionnalité de l'utilisation de la technologie CSS-in-JS.

Je dois dire que les balises <style> ne créent pas toutes les bibliothèques CSS-in-JS, car toutes ne sont pas basées sur les mécanismes qui fonctionnent lorsque les pages fonctionnent dans les navigateurs. Par exemple, la bibliothèque linaria ne fait rien du tout pendant que la page s'exécute dans le navigateur.

Il définit un ensemble de classes CSS fixes pendant le processus de génération du projet et ajuste la correspondance de toutes les règles dynamiques dans la chaîne de modèle (c'est-à-dire les règles CSS en fonction des propriétés du composant) avec des propriétés CSS personnalisées. Par conséquent, lorsqu'une propriété de composant change, la propriété CSS change et l'apparence de l'interface change. Grâce à cela, linaria est beaucoup plus rapide que les bibliothèques qui s'appuient sur des mécanismes qui fonctionnent pendant que les pages sont en cours d'exécution. Le fait est que lors de l'utilisation de cette bibliothèque, le système doit effectuer beaucoup moins de calculs lors du rendu des composants. La seule chose que vous devez faire lorsque vous utilisez linaria pendant le rendu est de vous rappeler de mettre à jour la propriété CSS personnalisée. En même temps, cependant, cette approche est incompatible avec IE11, elle a un support limité pour les propriétés CSS populaires et, sans configuration supplémentaire, ne vous permet pas d'utiliser des thèmes. Comme c'est le cas avec d'autres domaines du développement Web, parmi les bibliothèques CSS dans JS, il n'y en a pas d'idéal, adapté à toutes les occasions.

Résumé


À un moment donné, la technologie CSS-in-JS ressemblait à une révolution dans le domaine du style. Cela a facilité la vie de nombreux développeurs et a également permis, sans efforts supplémentaires, de résoudre de nombreux problèmes, tels que les collisions de noms et l'utilisation de préfixes de fabricants de navigateurs. Cet article est écrit pour faire la lumière sur la question de savoir comment les bibliothèques CSS-in-JS populaires (celles qui contrôlent les styles pendant qu'une page est en cours d'exécution) peuvent affecter les performances des projets Web. Je voudrais attirer particulièrement l'attention sur le fait que l'influence de ces bibliothèques sur les performances ne conduit pas toujours à l'apparition de problèmes perceptibles. En fait, dans la plupart des applications, cet effet est complètement invisible. Des problèmes peuvent survenir dans les applications dont les pages contiennent des centaines de composants complexes.

Les avantages de CSS-in-JS l'emportent généralement sur les effets négatifs potentiels de l'utilisation de cette technologie. Cependant, les inconvénients de CSS-in-JS doivent être gardés à l'esprit par les développeurs dont les applications rendent de grandes quantités de données, ceux dont les projets contiennent de nombreux éléments d'interface en constante évolution. Si vous pensez que votre application est soumise aux effets négatifs de CSS-in-JS, alors avant de refactoriser, cela vaut tout pour être correctement évalué et mesuré.

Voici quelques conseils pour améliorer les performances des applications qui utilisent les bibliothèques CSS-in-JS populaires qui font leur travail lorsque les pages s'exécutent dans un navigateur:

  1. Ne vous laissez pas trop emporter par la composition des composants stylisés. Essayez de ne pas répéter l'erreur dont j'ai parlé au début, et ne tentez pas, pour créer un bouton malheureux, de construire une composition de trois composants stylisés. Si vous souhaitez "réutiliser" le code, utilisez la propriété CSS et composez les chaînes de modèle. Cela vous permettra de vous passer des nombreux composants inutiles de Context.Consumer . En conséquence, React devra gérer moins de composants, ce qui augmentera la productivité du projet.
  2. Efforcez-vous d'utiliser des composants «statiques». Certaines bibliothèques CSS-in-JS optimisent le code généré si les styles du composant ne dépendent pas du thème ou des propriétés. Plus les chaînes de modèles sont «statiques», plus la probabilité que les scripts des bibliothèques CSS-in-JS s'exécutent plus rapidement est élevée.
  3. Essayez d'éviter les opérations de restitution inutiles de vos applications React. Efforcez-vous de ne rendre que lorsque vous en avez vraiment besoin. Grâce à cela, ni les actions React ni les actions de la bibliothèque CSS-in-JS ne se chargeront. Le nouveau rendu est une opération qui ne doit être effectuée que dans des cas exceptionnels. Par exemple - avec le retrait simultané d'un grand nombre de composants "lourds".
  4. Découvrez si une bibliothèque CSS-in-JS convient à votre projet qui n'utilise pas de scripts exécutés pendant que la page s'exécute dans le navigateur. Parfois, nous choisissons la technologie CSS-in-JS, car il est plus pratique pour le développeur de l'utiliser, et non diverses API JavaScript. Mais si votre application n'a pas besoin de support si elle n'a pas une utilisation intensive des propriétés CSS, alors il est fort possible qu'une bibliothèque CSS-in-JS comme linaria n'utilise pas de scripts qui s'exécutent pendant que la page est en cours d'exécution. De plus, cette approche réduira la taille du paquet d'applications d'environ 12 Ko. Le fait est que la taille du code de la plupart des bibliothèques CSS-in-JS tient dans 12-15 Ko, et le code de la même linaria est inférieur à 1 Ko.

Chers lecteurs! Utilisez-vous des bibliothèques CSS-in-JS?


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


All Articles