React. 5 Consejos para evitar “wasted renders”

5 técnicas para evitar los wasted renders de React y optimizar tu aplicación

Image for post
Image for post

Uno de los principales problemas a los que nos tenemos que enfrentar cuando intentamos optimizar el rendimiento de nuestras aplicaciones en React son los llamados wasted renders, es decir, renders de componentes cuyo estado interno no ha cambiado pero que debido a la forma en que funciona React son rendereados de nuevo durante el proceso de reconciliación del Virtual DOM con el DOM “real”.

En este artículo veremos algunas de las técnicas que podemos emplear para prevenirlos a la vez que profundizamos en la forma en que funciona React, de cara a ampliar nuestro conocimiento sobre esta librería. ¡Vamos allá!

Virtual DOM

Como ya os comentaba en un artículo anterior, React emplea el concepto de Virtual DOM con el fin de optimizar el proceso de pintar la interfaz gráfica de nuestra aplicación en función del estado que tenga en ese momento.

Para ello, React parte de un componente raíz a partir del cual se despliega todo el árbol de componentes, entendiendo como “componente” una función que renderiza una porción de la interfaz gráfica basada en los datos que posea, es decir, su estado y las props que reciba de su componente padre.

A medida que los usuarios interactúan con la interfaz gráfica (pulsaciones de botones que desencadenan llamadas AJAX, transiciones entre páginas, formularios…) se producen cambios en esos datos que provocan que la interfaz gráfica deba actualizarse para reflejarlos. Es decir:

El usuario no provoca directamente cambios en la interfaz gráfica sino en los datos / estado de la aplicación. Esos cambios son los que desencadenan la actualización de la interfaz gráfica.

A este respecto, es importante interiorizar que cuando hablamos de datos no sólo nos referimos a aquello que guardamos en la base de datos, sino también a todo lo asociado al estado de la interfaz gráfica (por ejemplo, la pestaña que está seleccionada o la slide que se está mostrando). De este modo, cuando cualquier parte de estos datos cambia, React calcula las diferencias entre la versión actual de la interfaz gráfica y la nueva en el Virtual DOM para actualizar las partes que así lo requieran de la forma más eficiente posible.

Sin embargo, este proceso de reconciliación entre el Virtual DOM y el DOM “real” puede ser una de las principales fuentes de problemas en lo que a rendimiento se refiere, pues puede provocar que al final la mayor parte de nuestra aplicación termine re-renderizándose.

Supongamos que partimos de un árbol como el siguiente:

Image for post
Image for post

Y que se produce un cambio en el componente G debido a un cambio en los datos presentes en R. Es decir, determinado dato es pasado desde R hasta el nodo G provocando en éste un cambio que tiene que reflejarse en la interfaz gráfica. Si React no estuviera desarrollado de forma eficiente, todo el árbol debería de renderearse de nuevo, pero sin embargo, lo que sucede es:

Image for post
Image for post

Es decir, React ha sido capaz de detectar que los nodos en gris no han sufrido ningún tipo de cambio por lo que no los renderizará de nuevo sino que solo los nodos R, B y G volverán a ser pintados. Por tanto, en lo que sigue de artículo veremos técnicas que nos permitan facilitar asegurar que este proceso de cálculo de diferencias React es capaz de resolverlo satisfactoriamente en términos de optimización y ahorrarnos así multitud de ciclos de CPU.

Identificando wasted renders

Lo primero de todo será aprender a identificar posibles wasted renders, para lo cual lo más fácil es activar la opción highlight updates en las React dev tools:

Image for post
Image for post

las cuáles podéis instalar desde aquí. De este modo, aquellos elementos de la interfaz gráfica se irán iluminando a medida que son rendereados permitiéndonos ver si determinados elementos de la interfaz están sufriendo renders innecesarios. En la propia documentación de React se nos muestra el siguiente ejemplo:

Image for post
Image for post

Como podéis ver, a medida que se escribe la segunda tarea, la primera también se está iluminando lo cual indica que se está rendereando aunque su estado no esté cambiando (este es el mejor ejemplo de un wasted render). Si nuestra aplicación cuenta con muchos casos de este tipo es probable que a la larga termine por sufrir problemas de rendimiento por lo que a continuación veremos algunas técnicas que nos ayudarán a evitarlos.

Método shouldComponentUpdate

Por defecto, React renderea el Virtual DOM y compara las diferencias de cada componente en el árbol en base a sus props y su estado interno. Pero, de cara a optimizar esta comparación, React nos provee además del método shouldComponentUpdate (integrado en el ciclo de vida del componente) que nos permite indicar si el componente debe o no actualizarse realizando para ello las comprobaciones que nosotros consideramos necesarias. Por defecto, la implementación de este método devuelve true pero nosotros podemos engancharnos a él para devolver lo que queramos en función de los cambios en las props o en el estado:

Image for post
Image for post

Pure Components

Cuando declaramos class components a menudo lo que hacemos es extender de React.Component . Sin embargo, React nos provee también de la clase React.PureComponent la cual cuenta con una implementación por defecto del método shouldComponentUpdate con una shallow comparison de las props y el estado. Por tanto, un PureComponent sólo es vuelto a renderearse si las props o el state cambian.

Recordad que en una shallow comparison los tipos de datos primitivos (como strings, booleans o numbers) son comparados por valor mientras que los tipos complejos (como arrays u objetos) son comparados por referencia. Es decir, aunque el contenido de dos objetos sea el mismo, la comparación devolverá false si ambos objetos poseen diferentes referencias en memoria.

React Memo

En el caso de que estemos trabajando con componentes funcionales, React nos provee de un High Order Component llamado React.memo que actúa del mismo modo que React.PureComponent pero para componentes funcionales:

Image for post
Image for post

Este HOC realiza también una shallow comparison de las props que recibe el componente pero nos da la opción de, por medio de un segundo argumento, implementar la comparación que queremos realizar del mismo modo que sucede con shouldComponentUpdate :

Image for post
Image for post

Datos inmutables

El “problema” como habéis visto de trabajar con React.PureComponent o React.memo se debe a la shallow comparison ya que podemos toparnos con dos casos:

  • que la referencia se haya modificado pero el valor siga siendo el mismo lo cual provocará un render innecesario
  • que la referencia siga siendo la misma pero el contenido haya cambiado lo cual provocará que no haya un render

Es en el segundo caso donde trabajar con datos inmutables cobra sentido.

La idea que subyace bajo este tipo de estructuras es muy simple: cuando hagamos cambios dentro de objetos o arrays, en vez de hacerlos sobre el mismo objeto los realizaremos sobre una copia del mismo obteniendo así una nueva referencia con los cambios realizados sobre el objeto original. Gracias a la llegada de ES6 este proceso se realiza de forma muy sencilla gracias al spread operator:

Image for post
Image for post

De este modo nos aseguraremos que siempre que se realice un cambio en un objeto u array obtendremos una nueva referencia.

Evitar pasar una nueva referencia para la misma información

Tal y como os comentaba unas líneas atrás otro problema de la shallow comparison es que si el objeto no ha cambiado pero pasamos una nueva referencia a él por la razón que sea, esta comparación devolverá false ya que las referencias son distintas, lo cual provocará un nuevo render.

Por tanto, dado que cada cambio en las props de un componente desencadena un render, es trabajo nuestro como desarrolladores asegurarnos de que mantendremos las mismas referencias a los objetos siempre que estos no cambien de modo que evitemos los conocidos wasted renders. Por ejemplo, suponed que tenemos el siguiente componente:

Image for post
Image for post

Y que recibimos mediante props una nueva lista de comentarios lo cual provoca un nuevo render del componente ArticleDetail. Lo que sucederá es que el componente ArticleBody también será rendereado debido a que cuando se ejecute el método render de ArticleDetail estamos creando una nueva referencia para la constante article y por tanto la shallow compariso dnel componente ArticleBody devolverá false .

Por tanto, la forma de resolverlo será pasar siempre la misma referencia al componente ArticleBody, por ejemplo:

Image for post
Image for post

Esto también se aplica a los event handlers o funciones. Por ejemplo, si tenemos el siguiente código:

Image for post
Image for post

Los dos componentes SomeComponent presentes serán rendereados siempre que ComponentOne lo haga, puesto que en ambos estamos creando una nueva función (es decir, una nueva referencia) en cada render (una función flecha en el primer caso y el resultado del bind en el segundo). La forma de resolver esto sería bien haciendo el bind en el constructor del componente o bien pasando siempre la misma referencia a la función:

Image for post
Image for post

Conclusión

Como habéis podido ver, existen distintas técnicas para tratar con los wasted renders y mejorar el rendimiento de la aplicación. Si conocéis alguna más que consideráis que debería estar añadida a este artículo dejadla en los comentarios. Hasta el siguiente artículo!

¿Quieres recibir más artículos como este?

Suscríbete a nuestra newsletter:

Written by

Entre paseo y paseo con Simba desarrollo en Symfony y React

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store