Cómo identificar y resolver renders desperdiciados en React

Entonces, recientemente estaba pensando en el perfil de rendimiento de una aplicación de reacción en la que estaba trabajando, y de repente pensé en establecer algunas métricas de rendimiento. Y me di cuenta de que lo primero que necesito abordar son los renders desperdiciados que estoy haciendo en cada una de las páginas web. Por cierto, podrías estar pensando en qué son los renders desperdiciados. Vamos a sumergirnos.

Desde el principio, React ha cambiado toda la filosofía de crear aplicaciones web y, posteriormente, la forma en que piensan los desarrolladores de aplicaciones para el usuario. Con la introducción de Virtual DOM , React hace que las actualizaciones de la interfaz de usuario sean lo más eficientes posible. Esto hace que la experiencia de la aplicación web sea ordenada. ¿Alguna vez te has preguntado cómo hacer que tus aplicaciones React sean más rápidas? ¿Por qué las aplicaciones web React de tamaño moderado todavía tienden a funcionar mal? ¡Los problemas radican en cómo usamos React!

Cómo funciona React

Una biblioteca de front-end moderna como React no hace que nuestra aplicación sea más rápida de manera asombrosa. Primero, los desarrolladores debemos entender cómo funciona React. ¿Cómo viven los componentes los ciclos de vida de los componentes durante la vida útil de las aplicaciones? Por lo tanto, antes de sumergirnos en cualquier técnica de optimización, debemos comprender mejor cómo funciona realmente React bajo el capó.

En el núcleo de React, tenemos la sintaxis JSX y la poderosa capacidad de React para construir y comparar DOM virtuales. Desde su lanzamiento, React ha influido en muchas otras bibliotecas frontales. Por ejemplo, Vue.js también se basa en la idea de DOM virtuales.

Cada aplicación de React comienza con un componente raíz. Podemos pensar en toda la aplicación como una formación de árbol donde cada nodo es un componente. Los componentes en React son 'funciones' que representan la interfaz de usuario en función de los datos. Eso significa apoyos y estado que recibe; decir que esCF

UI = CF(data)

Los usuarios interactúan con la interfaz de usuario y provocan el cambio en los datos. Las interacciones son cualquier cosa que un usuario pueda hacer en nuestra aplicación. Por ejemplo, hacer clic en un botón, deslizar imágenes, arrastrar elementos de la lista y solicitudes AJAX que invocan API. Todas esas interacciones solo cambian los datos. Nunca provocan ningún cambio en la interfaz de usuario.

Aquí, los datos son todo lo que define el estado de una aplicación. No solo lo que tenemos almacenado en nuestra base de datos. Incluso diferentes estados de la interfaz, como qué pestaña está seleccionada actualmente o si una casilla de verificación está marcada o no, forman parte de estos datos. Siempre que hay un cambio en los datos, React usa las funciones del componente para volver a renderizar la interfaz de usuario, pero solo virtualmente:

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

React calcula las diferencias entre la IU actual y la nueva IU aplicando un algoritmo de comparación en las dos versiones de su DOM virtual.

Changes = Difference(UI1, UI2)

React luego procede a aplicar solo los cambios de la interfaz de usuario a la interfaz de usuario real en el navegador. Cuando los datos asociados con un componente cambian, React determina si se requiere una actualización DOM real. Esto permite a React evitar operaciones de manipulación de DOM potencialmente costosas en el navegador. Ejemplos como crear nodos DOM y acceder a los existentes más allá de la necesidad.

Esta diferenciación y representación repetidas de componentes puede ser una de las principales fuentes de problemas de rendimiento de React en cualquier aplicación de React. Crear una aplicación React donde el algoritmo de diferenciación no se reconcilia de manera efectiva, lo que hace que toda la aplicación se procese repetidamente, lo que en realidad está causando renderizaciones desperdiciadas y eso puede resultar en una experiencia frustrantemente lenta.

Durante el proceso de renderizado inicial, React construye un árbol DOM como este:

Suponga que una parte de los datos cambia. Lo que queremos que React haga es volver a renderizar solo los componentes que se ven directamente afectados por ese cambio específico. Posiblemente omita incluso el proceso de diferenciación para el resto de componentes. Digamos que algunos datos cambian en el Componente 2en la imagen de arriba, y que los datos se ha transmitido de Ra By, a continuación 2. Si R vuelve a renderizar, volverá a renderizar a cada uno de sus hijos, es decir, A, B, C, D y, mediante este proceso, lo que realmente hace React es esto:

En la imagen de arriba, todos los nodos amarillos están renderizados y diferenciados. Esto da como resultado una pérdida de tiempo / recursos de cálculo. Aquí es donde pondremos principalmente nuestros esfuerzos de optimización. Configurar cada componente para renderizar y diferenciar solo cuando sea necesario. Esto nos permitirá recuperar esos ciclos de CPU desperdiciados. Primero, veremos la forma en que podemos identificar renders desperdiciados de nuestra aplicación.

Identificar renders desperdiciados

Hay algunas formas diferentes de hacer esto. El método más simple es activar la opción de actualizaciones destacadas en la preferencia de las herramientas de desarrollo de React.

Mientras interactúa con la aplicación, las actualizaciones se resaltan en la pantalla con bordes de colores. Mediante este proceso, debería ver los componentes que se han vuelto a renderizar. Esto nos permite detectar re-renderizaciones que no fueron necesarias.

Sigamos este ejemplo.

Tenga en cuenta que cuando ingresamos una segunda tarea, la primera "tarea" también parpadea en la pantalla con cada pulsación de tecla. Esto significa que React lo vuelve a representar junto con la entrada. Esto es lo que llamamos un render "desperdiciado". Sabemos que es innecesario porque el contenido de la primera tarea no ha cambiado, pero React no lo sabe.

Aunque React solo actualiza los nodos DOM modificados, volver a renderizar aún lleva algo de tiempo. En muchos casos, no es un problema, pero si la ralentización es notable, deberíamos considerar algunas cosas para detener esos renders redundantes.

Usando el método shouldComponentUpdate

De forma predeterminada, React renderizará el DOM virtual y comparará la diferencia de cada componente en el árbol para cualquier cambio en sus accesorios o estado. Pero eso obviamente no es razonable. A medida que nuestra aplicación crece, intentar volver a renderizar y comparar todo el DOM virtual en cada acción eventualmente ralentizará todo.

React proporciona un método de ciclo de vida simple para indicar si un componente necesita volver a renderizarse y, es decir, shouldComponentUpdatequé se activa antes de que comience el proceso de renderizado. Vuelve la implementación predeterminada de esta función true.

Cuando esta función devuelve verdadero para cualquier componente, permite que se active el proceso de diferenciación de renderizado. Esto nos da el poder de controlar el proceso de diferenciación del renderizado. Supongamos que necesitamos evitar que un componente se vuelva a renderizar, simplemente necesitamos regresar falsede esa función. Como podemos ver en la implementación del método, podemos comparar los accesorios y el estado actual y siguiente para determinar si es necesario volver a renderizar:

Usando componentes puros

A medida que trabaja en React, definitivamente lo sabe, React.Componentpero ¿de qué se trata React.PureComponent? Ya hemos discutido el método de ciclo de vida shouldComponentUpdate, en componentes puros, ya existe una implementación predeterminada de, shouldComponentUpdate()con una comparación superficial y de estado. Entonces, un componente puro es un componente que solo se vuelve a renderizar si props/statees diferente de los accesorios y el estado anteriores .

En una comparación superficial, los tipos de datos primitivos como cadena, booleano, número se comparan por valor y los tipos de datos complejos como matriz, objeto, función se comparan por referencia

Pero, ¿qué pasa si tenemos un componente funcional sin estado en el que necesitamos implementar ese método de comparación antes de que ocurra cada nueva representación? React tiene un componente de orden superior React.memo. Es como React.PureComponentpero para componentes funcionales en lugar de clases.

Por defecto, hace lo mismo que shouldComponentUpdate () que solo compara superficialmente el objeto props. Pero, ¿si queremos tener control sobre esa comparación? También podemos proporcionar una función de comparación personalizada como segundo argumento.

Hacer que los datos sean inmutables

¿Qué pasaría si pudiéramos usar un, React.PureComponentpero aún tener una forma eficiente de saber cuándo cualquier propiedad o estado complejo como una matriz, objeto, etc.ha cambiado automáticamente? Aquí es donde la estructura de datos inmutable facilita la vida.

La idea detrás del uso de estructuras de datos inmutables es simple. Como hemos comentado anteriormente, para los tipos de datos complejos, la comparación se realiza sobre su referencia. Siempre que un objeto que contiene datos complejos cambia, en lugar de realizar los cambios en ese objeto, podemos crear una copia de ese objeto con los cambios que crearán una nueva referencia.

ES6 tiene un operador de extensión de objetos para que esto suceda.

También podemos hacer lo mismo con las matrices:

Evite pasar una nueva referencia para los mismos datos antiguos

Sabemos que cada vez que propscambia el componente de un, se produce una nueva renderización. Pero a veces el propsno cambió. Escribimos el código de una manera que React cree que cambió, y eso también provocará una nueva renderización, pero esta vez es una renderización desperdiciada. Entonces, básicamente, debemos asegurarnos de que estamos pasando una referencia diferente como accesorios para diferentes datos. Además, debemos evitar pasar una nueva referencia para los mismos datos. Ahora, veremos algunos casos en los que estamos creando este problema. Veamos este código.

Aquí está el contenido del BookInfocomponente donde estamos renderizando dos componentes BookDescriptiony BookReview. Este es el código correcto y funciona bien, pero hay un problema. BookDescriptionse volverá a renderizar cada vez que obtengamos nuevos datos de revisiones como accesorios. ¿Por qué? Tan pronto como el BookInfocomponente recibe nuevos accesorios, renderse llama a la función para crear su árbol de elementos. La función de render crea una nueva bookconstante que significa que se crea una nueva referencia. Entonces, BookDescriptionobtendré esto bookcomo una referencia de noticias, que causará la repetición de BookDescription. Entonces, podemos refactorizar este fragmento de código a esto:

Ahora, la referencia es siempre la misma this.booky no se crea un nuevo objeto en el momento del render. Esta filosofía de re-renderización se aplica a todos los propcontroladores de eventos incluidos, como:

Aquí, hemos usado dos formas diferentes (métodos de enlace y uso de la función de flecha en el renderizado) para invocar los métodos del controlador de eventos, pero ambos crearán una nueva función cada vez que el componente se vuelva a renderizar. Entonces, para solucionar estos problemas, podemos vincular el método en constructory usando propiedades de clase que aún están en fase experimental y aún no están estandarizadas, pero muchos desarrolladores ya están usando este método para pasar funciones a otros componentes en aplicaciones listas para producción:

Terminando

Internamente, React utiliza varias técnicas inteligentes para minimizar la cantidad de costosas operaciones DOM necesarias para actualizar la interfaz de usuario. Para muchas aplicaciones, el uso de React conducirá a una interfaz de usuario rápida sin hacer mucho trabajo para optimizar el rendimiento específicamente. Sin embargo, si podemos seguir las técnicas que mencioné anteriormente para resolver renders desperdiciados, entonces para aplicaciones grandes también obtendremos una experiencia muy fluida en términos de rendimiento.