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é establecer algunas métricas de rendimiento. Y me di cuenta de que lo primero que necesito abordar son los desperdicios que estoy haciendo en cada una de las páginas web. ¿Podrías estar pensando en lo que son renders desperdiciados por cierto? 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 front-end. Con su 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 estamos usando React!

Cómo funciona React

Una biblioteca front-end moderna como React no hace que nuestra aplicación sea más rápida maravillosamente. Primero, los desarrolladores debemos entender cómo funciona React. ¿Cómo viven los componentes a través de los ciclos de vida de los componentes en la vida útil de las aplicaciones? Entonces, antes de sumergirnos en cualquier técnica de optimización, necesitamos tener una mejor comprensión de cómo React realmente funciona 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 front-end. Por ejemplo, Vue.js también se basa en la idea de DOM virtuales.

Cada aplicación 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 accesorios y estado que recibe; decir que es CF

UI = CF (datos)

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 AJAX solicita invocar API. Todas esas interacciones solo cambian los datos. Nunca causan 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 hemos almacenado en nuestra base de datos. Incluso diferentes estados frontales como qué pestaña está seleccionada actualmente o si una casilla de verificación está marcada actualmente o no son parte de estos datos. Cada vez que hay un cambio en los datos, React utiliza las funciones de los componentes para volver a representar la interfaz de usuario, pero solo virtualmente:

UI1 = CF (datos1)
UI2 = CF (datos2)

React calcula las diferencias entre la interfaz de usuario actual y la nueva interfaz de usuario mediante la aplicación de un algoritmo de comparación en las dos versiones de su DOM virtual.

Cambios = Diferencia (UI1, UI2)

React luego procede a aplicar solo los cambios de UI a la UI real en el navegador. Cuando los datos asociados con un componente cambian, React determina si se requiere una actualización DOM real. Esto permite que React evite 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 repetida diferenciación y representación de componentes puede ser una de las principales fuentes de problemas de rendimiento de React en cualquier aplicación React. Crear una aplicación React donde el algoritmo de diferenciación no se concilie de manera efectiva, lo que hace que toda la aplicación se procese repetidamente, lo que en realidad está causando renders desperdiciados y que puede resultar en una experiencia frustrantemente lenta.

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

Suponga que una parte de los datos cambia. Lo que queremos que haga React es volver a representar 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 los componentes. Digamos que algunos cambios de datos en el Componente 2 en la imagen de arriba, y esos datos se han pasado de R a B y luego a 2. Si R se vuelve a procesar, se volverá a representar cada uno de sus elementos secundarios, lo que significa A, B, C , D y mediante este proceso, lo que realmente hace React es esto:

En la imagen de arriba, todos los nodos amarillos se representan y diferencian. Esto resulta en tiempo perdido / recursos de cómputo. Aquí es donde principalmente pondremos nuestros esfuerzos de optimización. Configurar cada componente para representar y diferenciar solo cuando sea necesario. Esto nos permitirá reclamar esos ciclos de CPU desperdiciados. Primero, analizaremos la forma en que podemos identificar los 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 de resaltado en la preferencia de herramientas React dev.

Al interactuar 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 representar. Esto nos permite detectar reproducciones que no eran necesarias.

Sigamos este ejemplo.

Tenga en cuenta que cuando ingresamos un segundo todo, el primer "todo" también parpadea en la pantalla en 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 primer contenido de tareas pendientes no ha cambiado, pero React no lo sabe.

A pesar de que React solo actualiza los nodos DOM modificados, la re-representación todavía lleva algún tiempo. En muchos casos, no es un problema, pero si la desaceleración es notable, deberíamos considerar algunas cosas para detener esos renders redundantes.

Usando el método shouldComponentUpdate

Por defecto, React renderizará el DOM virtual y comparará la diferencia para 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 una nueva representación y, por ejemplo, shouldComponentUpdate, que se activa antes de que comience el proceso de representación. La implementación predeterminada de esta función devuelve verdadero.

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 render. Supongamos que necesitamos evitar que un componente se vuelva a representar, simplemente necesitamos devolver falso de esa función. Como podemos ver en la implementación del método, podemos comparar los accesorios y el estado actuales y siguientes para determinar si es necesario volver a renderizar:

Usando componentes puros

Mientras trabajas en React, definitivamente sabes sobre React.Component, pero ¿cuál es el trato con React.PureComponent? Ya hemos discutido el método del ciclo de vida shouldComponentUpdate, en componentes puros, ya existe una implementación predeterminada de shouldComponentUpdate () con una comparación superficial y de estado. Por lo tanto, un componente puro es un componente que solo se vuelve a representar si los accesorios / estado son diferentes 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 representación? React tiene un componente de orden superior React.memo. Es como React.PureComponent pero para componentes funcionales en lugar de clases.

Por defecto, hace lo mismo que shouldComponentUpdate () que solo compara superficialmente el objeto de utilería. 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.PureComponent pero aún así tener una manera eficiente de saber cuándo algún accesorio 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. Cada vez que un objeto que contiene datos complejos cambia, en lugar de hacer 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 propagación de objetos para que esto suceda.

También podemos hacer lo mismo para las matrices:

Evite pasar una nueva referencia para los mismos datos antiguos

Sabemos que cada vez que cambian los accesorios de un componente, se produce un renderizado. Pero a veces los accesorios no cambiaron. Escribimos el código de una manera que React cree que sí cambió, y eso también causará un re-render pero esta vez es un render desperdiciado. 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 para el componente BookInfo donde representamos dos componentes, BookDescription y BookReview. Este es el código correcto y funciona bien, pero hay un problema. BookDescription se volverá a representar cada vez que obtengamos nuevos datos de reseñas como accesorios. ¿Por qué? Tan pronto como el componente BookInfo recibe nuevos accesorios, se llama a la función de renderizado para crear su árbol de elementos. La función de renderizado crea una nueva constante de libro que significa que se crea una nueva referencia. Por lo tanto, BookDescription obtendrá este libro como referencia de noticias, lo que provocará la reproducción de BookDescription. Entonces, podemos refactorizar este fragmento de código para esto:

Ahora, la referencia es siempre la misma, este.book y un nuevo objeto no se crean en el momento del renderizado. Esta filosofía de representación se aplica a todos los accesorios, incluidos los controladores de eventos, como:

Aquí, hemos utilizado 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 vuelva a renderizarse. Entonces, para solucionar estos problemas, podemos vincular el método en el constructor y usar propiedades de clase que todavía están en fase experimental y aún no estandarizadas, pero muchos desarrolladores ya están usando este método de 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 IU. Para muchas aplicaciones, el uso de React conducirá a una interfaz de usuario rápida sin hacer mucho trabajo para optimizar específicamente el rendimiento. Sin embargo, si podemos seguir las técnicas que he mencionado anteriormente para resolver renders desperdiciados, para aplicaciones grandes también obtendremos una experiencia muy fluida en términos de rendimiento.