Mejorar el rendimiento de una aplicaci贸n con ReactJS

En toda aplicaci贸n con ReactJS mediana o grande siempre llegar谩 el momento en que necesitamos mejorar el rendimiento de la aplicaci贸n, este es el punto en el que te puedes detener a pensar en que el rendimiento depende de muchas cosas, el navegador que ejecuta el c贸digo, la versi贸n del navegador, el dispositivo en donde se est谩 ejecutando nuestra aplicaci贸n, la latencia de la red que tiene el usuario, todos estos puntos son importantes y debemos tenerlos en cuenta, pero en este art铆culo te quiero compartir lo que podamos hacer desde el lado de ReactJS para mejorar el rendimiento en general.

El rendimiento se basa en la cantidad de CPU y Memoria RAM que requiere nuestra aplicaci贸n para poder cargar o ser usada, un buen ejemplo de una aplicaci贸n pesada que est谩 bien optimizada es Google Maps. Pero la regla general para medir el rendimiento es que entre menos CPU y RAM consumas es mejor.

Antes tambi茅n mencione la latencia, que en otras palabras es el tiempo que se demora tu aplicaci贸n en requerir informaci贸n desde el servidor y recibirla, hay varias t茅cnicas para mejorar estos tiempos, como CDN, balanceadores de carga u otros externos a ReactJS.

Antes de ir a los puntos quiero recordarte que la optimizaci贸n temprana es uno de los peores errores que podemos cometer, la mayor铆a de los puntos siguientes son para atacar requerimientos espec铆ficos, en la mayor铆a de los casos ReactJS logra cubrir casos extremos de uso de recursos sin que lo notemos, pero si llegas a tener un caso en que necesita algo de ayuda para lograrlo pues aqu铆 vamos.

Consumo de CPU

El uso de CPU es el resultado de la ejecuci贸n del c贸digo, cada vez que cargamos una vista y sus componentes, cargamos y ejecutamos todo el c贸digo que incluye, se puede resumir mencionando que cada vez que renderizamos un componente, ejecutamos una funci贸n o hacemos un request de datos externos estamos usando CPU.

Evita renders innecesarios

Un componente se renderiza de nuevo cada vez que sus props o states cambian, en un counter cada vez que le damos click al bot贸n de + el valor del estado cambia, esto hace que el componente que recibe la cuenta se actualice con el nuevo valor y a su vez actualiza la UI, todo este proceso consume recursos. Si renderizamos un componente sin ning煤n cambio estamos consumiendo recursos innecesarios.

Para indicarle a ReactJS que debe validar los valores de los props podamos usar PureComponent en los componentes de clases o memo para los componentes funcionales, ambos m茅todos hacen una comparaci贸n de primer nivel en las propiedades que recibe un componente.

Ejemplo de Memo:

const Componente() { return (...) } export default memo(Componente)

Pero ya que en nuestro amado JavaScript {} === {} es false cuando tenemos propiedades que son arreglos u objetos o si quieres que tu componente se renderize 煤nicamente cuando una propiedad de tu componente cambia debes usar el segundo par谩metro que recibe memo que es una funci贸n:

// siguiendo el ejemplo anterior const arePropsEqual = (prevProps, nextProps) => { return prevProps.propiedad === nextProps.propiedad; } export default memo(Componente, arePropsEqual);

En este caso, mientras que la funci贸n eval煤e a true indicar谩 que las propiedades son iguales y que el componente no debe actualizarse.

No se recomienda hacer operaciones sobre un array directamente sobre el prop:

<Componente nombres={arreglo.filter((elemento) => elemento.propiedad === valor)} ... />

Esto generar谩 que cada vez que se filter el arreglo cambiar谩 todo el valor de nombres y requerir谩 renderizar de nuevo el componente y sus hijos, en este caso es mejor realizar el filtro dentro del componente y tener en cuenta que cada vez que se pueda es mejor evitar pasarle arreglos u objetos como propiedades a un componente.

Manejar la ejecuciones de las funciones

Considera una funci贸n que se ejecuta cada vez que escribes una letra en un input, genera un request al servidor que busca datos, si queremos buscar "esternocleidomastoideo" al comenzar a escribir se estar谩 ejecutando esta funci贸n, generando solicitudes al servidor, actualizando las propiedades y requiriendo que los componentes se rendericen nuevamente.

Aqu铆 entran a jugar t茅cnicas como debounce lodash tiene una api que nos permita usarlo directamente, este m茅todo previene que una funci贸n se llame indiscriminadamente, si definimos un delay de 500ms luego de la primera ejecuci贸n, la aplicaci贸n espera 500 ms antes de volver a ejecutar la funci贸n, por ejemplo:

1const debouncedSearch = _.debounce ((e) => { 2 const value = e.target.value; 3 fetch ( 4 ... 5 ); 6}, 500); 7 8render () { 9 return ( 10 <input onChange = {debouncedSearch} ... /> 11 ) 12}

Disminuir la cantidad de c贸digo en la p谩gina

Mientras menos c贸digo tenga nuestra aplicaci贸n, ser谩 m谩s f谩cil de cargar en el cliente. En aplicaciones que se renderizan desde el lado del servidor no tenemos mucho que hacer, con cada cambio de p谩gina traeremos lo necesario de cada p谩gina preprocesado por el servidor, pero en aplicaciones que se ejecutan 煤nicamente del lado del cliente debemos considerar todo lo que nos ayude a disminuir el tama帽o del bundle.

El bundle es el paquete de c贸digo JavaScript que requiere nuestra aplicaci贸n para funcionar, cada l铆nea de c贸digo que escribimos m谩s el c贸digo que incluyen los paquetes externos que instalamos suman, para controlar el tama帽o de los paquetes externos podemos usar herramientas como bundlephobia que nos muestr谩n cuanto peso agrega una librer铆a de npm a nuestro bundle final en producci贸n.

Para el c贸digo de nuestra aplicaci贸n ReactJS tiene disponible las api de lazy y Suspense code splitting. Lazy se utiliza para cargar los componentes o vistas que requiere nuestra aplicaci贸n de forma as铆ncrona, es decir que el bundle de nuestra aplicaci贸n se dividir谩 en partes que tendr谩n relaci贸n con la vista que estamos viendo, al momento de entrar a la ruta /home se cargar谩 el c贸digo JavaScript asociado a esta ruta y as铆 sucesivamente con todas las rutas que se carguen de forma lazy.

Suspense por otro lado es un agrupador de componentes o rutas Lazy, es el encargado de controlar el "suspenso" que hay en la aplicaci贸n desde el monto que solicitamos los componentes de una nueva vista, hasta que estos se cargan efectivamente, para lograr esto recibe una propiedad fallback donde recibe el componente que se montar谩 durante el suspenso o intercambio de una vista a otra.

En el ejemplo de la documentaci贸n de ReactJS podemos ver c贸mo se implementa:

import React from 'react'; const OtherComponent = React.lazy(() => import('./OtherComponent')); const AnotherComponent = React.lazy(() => import('./AnotherComponent')); function MyComponent() { return ( <div> <React.Suspense fallback={<div>Loading...</div>}> <section> <OtherComponent /> <AnotherComponent /> </section> </React.Suspense> </div> ); }

Disminuir consumo de memoria

El consumo de memoria est谩 asociado a la cantidad de datos que tenemos almacenados en tiempo de ejecuci贸n de nuestra aplicaci贸n, los estados de cada componente se guardan en memoria, en una aplicaci贸n de ReactJS sin librer铆as que persistan el valor de los estados cada vez que se desmonta un componente de la vista el valor de estos estados se limpia y no suelen ser un problema, pero si tenemos una implementaci贸n que requiere persistir alg煤n estado del lado del cliente como Redux o alg煤n otro State Manager que mantenga el valor cargado podemos comenzar a tener problemas.

Reducir el tama帽o del Storage

Asumiendo que el Storage es el lugar donde guardamos el valor del estado de nuestra aplicaci贸n, lo mejor es ir cargando los datos que vamos necesitando de forma recursiva y justo lo necesario, sabemos que mientras m谩s datos tenemos del lado del cliente podemos hacer c谩lculos m谩s r谩pidamente o actualizaciones de la UI, pero hay que saber d贸nde marcar la l铆nea.

Al cargar la informaci贸n de forma diferida generamos mayor cantidad de solicitudes de datos al servidor y esto tambi茅n puede ser un problema por la latencia, por eso la linea de qu茅 datos guardar en el cliente y cu谩les solicitar al servidor depende mucho de la aplicaci贸n o negocio, por ejemplo si tenemos una tienda con 3 productos podr铆amos cargar la data de los 3 productos completa sin problemas, pero si tenemos una tienda con 100 productos quiz谩s sea mejor cargar solo la informaci贸n preliminar de los productos para poder mostrar una lista y al momento de que el usuario seleccione uno de estos ir al servidor a buscar la informaci贸n detallada del producto, en este punto podemos decidir si guardar en momer铆a la informaci贸n asociada a ese producto por si la vuelve a consultar o sustituirla por otro producto si visita otro.

Una t茅cnica que puede ser intermedia es guardar los datos en disco, para eso los navegadores nos disponibilizan la api de IndexedDB podemos guardar la informaci贸n y al momento de requerirla la vamos a buscar de forma local en el cliente, sin necesidad de ir al servidor.

Estar atentos de los Memory Leak

Los Memory Leak o P茅rdidas de memoria, ocurren en los lenguajes como JavaScirpt que tienen "Recolectores de Basura" tambi茅n conocidos como Trash Collector que se ejecutan peri贸dicamente en tiempo de ejecuci贸n para limpiar de la memoria las variables o funciones que ya no est谩n en uso.

En ReactJS podemos ver estos errores cuando ejecutamos una solicitud al servidor de informaci贸n que se debe cargar en el estado de un componente, pero antes de que el servidor responda se desmonta en componente, en estos casos debemos poder cancelar la petici贸n hecha al servidor para evitar que se carguen esos datos en memoria que no ser谩n consumidos por ning煤n componente.

Encontr茅 ejemplos de como implementar el cancelar peticiones al servidor con axios y con fetch.

Teniendo en cuenta de que todos los estados o funciones que cargan en memoria cuando el componente se monta (ciclo de vida componentDidMount()) se debe de descargar de la memoria cuando se desmonta el componente (ciclo de vida componentWillUnmount).

Esto se vuelve un poco m谩s complejo de comprender en los componentes funcionales ya que ambos ciclos de vida ocurren dentro de useEffect pero si montamos un listener la acci贸n sobre un bot贸n debemos retornar la cancelaci贸n de ese es listener al momento de desmontar el componente, por ejemplo:

1... 2 const funcQueHaceAlgo = () => {...} 3 4 useEffect(() => { 5 window.addEventListener('keydown', funcQueHaceAlgo); 6 7 return () => { 8 window.removeEventListener('keydown', funcQueHaceAlgo); 9 }; 10 }); 11...

La funci贸n debe estar declarada y asignada a una variable, no se deben utilizar funciones an贸nimas ya que la funci贸n dentro de addEventListener tendr谩 un espacio de memoria diferente a la funci贸n dentro de removeEventListener.

Tambi茅n nos podemos ayudar con el DevTool de Chrome para encontrar problemas de memoria, por ac谩 un art铆culo al respecto.

Espero que esta informaci贸n te sea 煤til y logres mejorar el rendimiento de tu aplicaci贸n, recuerda que el uso de CPU y Memoria repercute tambi茅n en el uso de electricidad, si nuestra aplicaci贸n consume recursos en exceso y es abierta desde un dispositivo m贸vil podemos estar consumiendo m谩s bater铆a del usuario.

Actualizado 28/11/2020 a las 23:52