El precio oculto de las bibliotecas CSS-in-JS en las aplicaciones React

En las aplicaciones de front-end modernas, la tecnología CSS-in-JS es bastante popular. Lo que pasa es que brinda a los desarrolladores un mecanismo para trabajar con estilos que es más conveniente que el CSS normal. No me malinterpretes. Realmente me gusta CSS, pero crear una buena arquitectura CSS no es una tarea fácil. La tecnología CSS-in-JS ofrece algunas ventajas significativas sobre los estilos CSS convencionales. Pero, desafortunadamente, el uso de CSS-in-JS puede, en ciertas aplicaciones, conducir a problemas de rendimiento. En este artículo, trataré de analizar las características de alto nivel de las bibliotecas CSS-in-JS más populares, hablaré sobre algunos de los problemas que a veces surgen al usarlas y sugeriré formas de mitigar estos problemas.



Resumen de la situación


En mi empresa, se decidió crear una biblioteca de interfaz de usuario. Esto nos brindaría un beneficio considerable, nos permitiría reutilizar fragmentos estándar de interfaces en varios proyectos. Fui uno de los voluntarios que asumió la tarea. Decidí usar la tecnología CSS-in-JS, ya que estaba familiarizado con la API de diseño de las bibliotecas CSS-in-JS más populares. En el curso del trabajo, me esforcé por actuar razonablemente. Diseñé lógica reutilizable y apliqué propiedades compartidas en componentes. Por lo tanto, tomé la composición de los componentes. Por ejemplo, el <IconButton /> extendió el componente <BaseButton /> , que, a su vez, fue una implementación de una entidad simple con styled.button . Desafortunadamente, resultó que IconButton necesita su propio estilo, por lo que convertí este componente en un componente estilizado:

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

A medida que aparecían más y más componentes en nuestra biblioteca, usábamos más y más operaciones de composición. Esto no parecía antinatural. Después de todo, la composición es, de hecho, la base de React. Todo estuvo bien hasta que creé el componente Table . Comencé a sentir que este componente se procesaba lentamente. Especialmente en situaciones donde el número de filas en la tabla excedió 50. Esto era incorrecto. Por lo tanto, comencé a comprender el problema recurriendo a las herramientas de desarrollo.

Por cierto, si alguna vez se preguntó por qué las reglas CSS no se pueden editar con el inspector de herramientas de desarrollador, tenga en cuenta que esto se debe a que usan CSSStyleSheet.insertRule () . Esta es una forma muy rápida de modificar hojas de estilo. Pero uno de sus inconvenientes es el hecho de que el inspector ya no puede editar las hojas de estilo correspondientes.

No hace falta decir que el árbol generado por React fue realmente enorme. La cantidad de componentes de Context.Consumer fue tan grande que podría privarme del sueño. El hecho es que cada vez que se representa un único componente con estilo usando componentes con estilo o emoción , además de crear un componente React regular, también se Context.Consumer un componente Context.Consumer adicional. Esto es necesario para permitir que la secuencia de comandos correspondiente (la mayoría de las bibliotecas CSS-in-JS dependen de las secuencias de comandos que se ejecutan mientras se ejecuta la página) para procesar correctamente las reglas de estilo generadas. Por lo general, esto no causa ningún problema especial, pero no debemos olvidar que los componentes deben tener acceso al tema. Esto se traduce en la necesidad de representar Context.Consumer adicional para cada elemento estilizado, lo que le permite "leer" el tema desde el componente ThemeProvider . Como resultado, al crear un componente estilizado en una aplicación con un tema, se crean 3 componentes. Este es un componente StyledXXX y dos componentes más Context.Consumer .

Es cierto que no hay nada particularmente terrible aquí, ya que React hace su trabajo rápidamente, lo que significa que en la mayoría de los casos no tenemos nada de qué preocuparnos. Pero, ¿qué sucede si se ensamblan varios componentes estilizados para crear un componente más complejo? ¿Qué sucede si este componente complejo es parte de una lista larga o una tabla grande donde se representan al menos 100 de estos componentes? Aquí en tales situaciones, nos enfrentamos a problemas ...

Perfilado


Para probar diferentes soluciones CSS-in-JS, creé una aplicación simple. Muestra el texto Hello World 50 veces. En la primera versión de esta aplicación, envolví este texto en un elemento div regular. En el segundo , utilicé el componente styled.div . Además, agregué un botón a la aplicación que hace que estos 50 elementos se vuelvan a representar.

Después de representar el componente <App /> , se mostraron dos árboles React diferentes. Las siguientes figuras muestran los árboles de elementos deducidos por React.


Un árbol que se muestra en una aplicación que usa un elemento div regular


Un árbol que se muestra en una aplicación que usa styled.div

Luego, usando el botón, rendericé <App /> 10 veces para recopilar datos sobre la carga en el sistema, que Context.Consumer componentes adicionales de Context.Consumer . Aquí hay información sobre volver a representar repetidamente una aplicación con elementos div regulares en modo de diseño.


Vuelva a representar la aplicación con elementos div regulares en modo de diseño. El valor promedio es 2.54 ms.


Vuelva a representar la aplicación con elementos styled.div en modo de desarrollo. El valor promedio es 3.98 ms.

Lo que es muy interesante es que, en promedio, una aplicación CSS-in-JS es 56.6% más lenta de lo habitual. Pero era un modo de desarrollo. ¿Qué pasa con el modo de producción?


Vuelva a representar la aplicación con los elementos div habituales en modo de producción. El valor promedio es 1.06 ms.


Vuelva a representar la aplicación con elementos styled.div en modo de producción. El valor promedio es 2.27 ms.

Cuando el modo de producción está activado, la implementación div de la aplicación parece ser más de un 50% más rápida en comparación con la misma versión en modo de desarrollo. Una aplicación styled.div es solo un 43% más rápida. Y aquí, como antes, está claro que la solución CSS-in-JS es casi el doble de lenta que la solución habitual. ¿Qué lo frena?

Análisis de la aplicación durante su ejecución.


La respuesta obvia a la pregunta de qué ralentiza una aplicación CSS-in-JS puede ser la siguiente: "Se dijo que cien bibliotecas CSS-in-JS representan dos Context.Consumer por componente". Pero si piensa en todo esto Context.Consumer , Context.Consumer es solo un mecanismo para acceder a una variable JS. Por supuesto, React necesita hacer algo de trabajo para descubrir dónde leer el valor correspondiente, pero esto solo no explica los resultados de medición anteriores. La respuesta real a esta pregunta se puede encontrar analizando la razón para usar Context.Consumer . El hecho es que la mayoría de las bibliotecas CSS-in-JS dependen de scripts que se ejecutan durante la salida de la página en el navegador, lo que ayuda a las bibliotecas a actualizar dinámicamente los estilos de los componentes. Estas bibliotecas no crean clases CSS durante el ensamblaje de la página. En cambio, generan y actualizan dinámicamente etiquetas <style> en el documento. Esto se hace cuando se montan los componentes o cuando cambian sus propiedades. Estas etiquetas generalmente contienen una sola clase CSS cuyo nombre hash se asigna a un solo componente React. Cuando las propiedades de este componente cambian, la etiqueta <style> correspondiente también debe cambiar. Aquí se describe cómo se hace durante este proceso:

  • Las reglas CSS que debe tener la etiqueta <style> se vuelven a generar.
  • Se crea un nuevo nombre de clase hash que se utiliza para almacenar las reglas CSS mencionadas anteriormente.
  • La propiedad classname del componente React correspondiente se actualiza a una nueva, lo que indica la clase que acaba de crear.

Considere, por ejemplo, la biblioteca de styled-components . Al crear el componente styled.div biblioteca asigna un identificador interno (ID) a este componente y agrega una nueva etiqueta <style> a la etiqueta HTML <head> . Esta etiqueta contiene un solo comentario que se refiere al identificador interno del componente React al que pertenece el estilo correspondiente:

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

Y aquí están las acciones que realiza la biblioteca de componentes con estilo al mostrar el componente React correspondiente:

  1. Analiza las reglas CSS de la cadena de plantilla de componentes con estilo.
  2. Genera un nuevo nombre de clase CSS (o descubre si se debe mantener el nombre anterior).
  3. Realiza el preprocesamiento de estilos usando stylis.
  4. Incrusta el CSS resultante del preprocesamiento en la etiqueta <style> correspondiente de la etiqueta <head> .

Para poder utilizar el tema en el paso 1 de este proceso, se requiere Context.Consumer . Gracias a este componente, los valores se leen del tema en la cadena de plantilla. Para poder modificar la etiqueta <style> asociada con este componente, Context.Consumer necesita un Context.Consumer más del componente. Le permite acceder a una instancia de una hoja de estilo. Es por eso que en la mayoría de las bibliotecas CSS-in-JS encontramos dos instancias de Context.Consumer .

Además de esto, dado que todos estos cálculos afectan la interfaz de usuario, debe tenerse en cuenta que deben realizarse durante la fase de representación del componente. No pueden ejecutarse en el código de los controladores de eventos para el ciclo de vida de los componentes React (de esta forma, su ejecución puede retrasarse y parecerá una formación de página lenta para el usuario). Es por eso que la representación de aplicaciones styled.div es más lenta que la representación de una aplicación normal.

Todo esto ha sido notado por los desarrolladores de componentes con estilo. Optimizaron la biblioteca para reducir el tiempo de representación de componentes. En particular, la biblioteca descubre si el componente estilizado es "estático". Es decir, si los estilos del componente dependen del tema o de las propiedades que se le pasan. Por ejemplo, el componente estático se muestra a continuación:

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

Y este componente no es estático:

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

Si la biblioteca descubre que el componente es estático, omite los 4 pasos anteriores y se da cuenta de que nunca tendrá que cambiar el nombre de clase generado (ya que no hay un elemento dinámico para el que sea necesario cambiar las reglas CSS asociadas a él). Además, en esta situación, la biblioteca no muestra ThemeContext.Consumer alrededor del componente estilizado, ya que la dependencia del tema ya no permitiría que el componente se considere "estático".

Si fue lo suficientemente cuidadoso al analizar las capturas de pantalla presentadas anteriormente, entonces podría notar que incluso en el modo de producción para cada styled.div se Context.Consumer dos componentes Context.Consumer . Curiosamente, el componente que se procesó fue "estático", ya que no se asociaron reglas CSS dinámicas. En tal situación, uno esperaría que si este ejemplo se escribiera usando la biblioteca de componentes con estilo, no Context.Consumer necesario para trabajar con el tema. La razón por la que se muestran exactamente dos Context.Consumer aquí es porque el experimento, cuyos datos se dan arriba, se llevó a cabo utilizando la emoción, otra biblioteca CSS-in-JS. Esta biblioteca adopta casi el mismo enfoque que los componentes con estilo. Las diferencias entre ellos son pequeñas. Por lo tanto, la biblioteca de emociones analiza la cadena de la plantilla, procesa previamente los estilos usando stylis y actualiza el contenido de la <style> correspondiente. Aquí, sin embargo, debe notarse una diferencia clave entre los componentes con estilo y la emoción. Consiste en el hecho de que la biblioteca de emociones siempre envuelve todos los componentes en ThemeContext.Consumer , independientemente de si usan el tema o no (esto explica la apariencia de la captura de pantalla anterior). Curiosamente, a pesar de que la emoción representa más componentes del consumidor que los componentes con estilo, las emociones superan a los componentes con estilo en términos de rendimiento. Esto indica que el número de componentes de Context.Consumer no es un factor importante para ralentizar el renderizado. Vale la pena señalar que al momento de escribir este material, se lanzó una versión beta de componentes con estilo v5.xx que, según los desarrolladores de la biblioteca, evita la emoción en términos de rendimiento.

Resume de lo que estábamos hablando. Resulta que una combinación de muchos elementos Context.Consumer (lo que significa que React tiene que coordinar el trabajo de elementos adicionales) y los mecanismos internos de diseño dinámico pueden ralentizar la aplicación. Debo decir que todas las etiquetas <style> agregadas al <head> para cada componente nunca se eliminan. Esto se debe al hecho de que las operaciones de eliminación de elementos crean una gran carga en el DOM (por ejemplo, el navegador debe reorganizar la página debido a esto). Esta carga es mayor que la carga adicional en el sistema causada por la presencia en la página de elementos <style> innecesarios. Para ser sincero, no puedo decir con confianza que las etiquetas <style> innecesarias pueden causar problemas de rendimiento. Simplemente almacenan las clases no utilizadas generadas durante el funcionamiento de la página (es decir, estos datos no se transmitieron a través de la red). Pero debe conocer esta característica del uso de la tecnología CSS-in-JS.

Debo decir que las etiquetas <style> no crean todas las bibliotecas CSS-in-JS, ya que no todas se basan en los mecanismos que funcionan cuando las páginas funcionan en los navegadores. Por ejemplo, la biblioteca linaria no hace nada mientras la página se ejecuta en el navegador.

Define un conjunto de clases CSS fijas durante el proceso de construcción del proyecto y ajusta la correspondencia de todas las reglas dinámicas en la cadena de plantilla (es decir, las reglas CSS que dependen de las propiedades del componente) con propiedades CSS personalizadas. Como resultado, cuando cambia la propiedad de un componente, cambia la propiedad CSS y cambia el aspecto de la interfaz. Gracias a esto, linaria es mucho más rápido que las bibliotecas que dependen de mecanismos que funcionan mientras se ejecutan las páginas. La cuestión es que cuando se usa esta biblioteca, el sistema tiene que realizar mucho menos cómputo durante la representación de componentes. Lo único que debe hacer cuando usa linaria durante el renderizado es recordar actualizar la propiedad CSS personalizada. Al mismo tiempo, sin embargo, este enfoque es incompatible con IE11, tiene soporte limitado para propiedades CSS populares y, sin configuración adicional, no le permite usar temas. Como es el caso con otras áreas de desarrollo web, entre las bibliotecas CSS-in-JS no hay una ideal, adecuada para todas las ocasiones.

Resumen


La tecnología CSS-in-JS en un momento parecía una revolución en el campo del estilo. Hizo la vida más fácil para muchos desarrolladores y también permitió, sin esfuerzos adicionales, resolver muchos problemas, como las colisiones de nombres y el uso de prefijos del fabricante del navegador. Este artículo está escrito para arrojar algo de luz sobre la cuestión de cómo las bibliotecas populares CSS-in-JS (aquellas que controlan los estilos mientras se ejecuta una página) pueden afectar el rendimiento de los proyectos web. Me gustaría llamar la atención sobre el hecho de que la influencia de estas bibliotecas en el rendimiento no siempre conduce a la aparición de problemas notables. De hecho, en la mayoría de las aplicaciones este efecto es completamente invisible. Pueden ocurrir problemas en aplicaciones que tienen páginas que contienen cientos de componentes complejos.

Los beneficios de CSS-in-JS generalmente superan los posibles efectos negativos del uso de esta tecnología. Sin embargo, las desventajas de CSS-in-JS deben ser tenidas en cuenta por aquellos desarrolladores cuyas aplicaciones procesan grandes cantidades de datos, aquellos cuyos proyectos contienen muchos elementos de interfaz que cambian constantemente. Si sospecha que su aplicación está sujeta a los efectos negativos de CSS-in-JS, entonces, antes de refactorizar, vale la pena evaluar y medir adecuadamente todo.

Estos son algunos consejos para mejorar el rendimiento de las aplicaciones que utilizan las bibliotecas populares CSS-in-JS que hacen su trabajo cuando las páginas se ejecutan en un navegador:

  1. No te dejes llevar por la composición de los componentes estilizados. Intente no repetir el error del que hablé al principio, y no intente, para crear un botón desafortunado, construir una composición de tres componentes estilizados. Si desea "reutilizar" el código, use la propiedad CSS y redacte cadenas de plantillas. Esto le permitirá prescindir de los muchos componentes innecesarios de Context.Consumer . Como resultado, React tendrá que administrar menos componentes, lo que aumentará la productividad del proyecto.
  2. Esforzarse por usar componentes "estáticos". Algunas bibliotecas CSS-in-JS optimizan el código generado si los estilos del componente no dependen del tema o las propiedades. Cuanto más "estática" haya en las cadenas de plantilla, mayor será la probabilidad de que los scripts en las bibliotecas CSS-in-JS se ejecuten más rápido.
  3. Intente evitar operaciones innecesarias de representación de sus aplicaciones React. Esfuércese por renderizar solo cuando realmente lo necesite. Gracias a esto, ni las acciones React ni las acciones de la biblioteca CSS-in-JS se cargarán. La representación nueva es una operación que solo debe realizarse en casos excepcionales. Por ejemplo, con la retirada simultánea de una gran cantidad de componentes "pesados".
  4. Averigüe si una biblioteca CSS-in-JS es adecuada para su proyecto que no utiliza scripts que se ejecutan mientras la página se ejecuta en el navegador. A veces elegimos la tecnología CSS-in-JS porque es más conveniente para el desarrollador usarla, y no varias API de JavaScript. Pero si su aplicación no necesita soporte si no tiene un uso intensivo de las propiedades CSS, entonces es posible que pueda usar una biblioteca CSS-in-JS como linaria que no usa scripts que se ejecutan mientras se ejecuta la página. Este enfoque, además, reducirá el tamaño del paquete de aplicaciones en aproximadamente 12 Kb. El hecho es que el tamaño del código de la mayoría de las bibliotecas CSS-in-JS cabe en 12-15 Kb, y el código de la misma linaria es inferior a 1 Kb.

Estimados lectores! ¿Utiliza bibliotecas CSS-in-JS?


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


All Articles