Componentes eficientes de React: una guía para optimizar el rendimiento de React

Publicado: 2022-03-11

Desde su introducción, React ha cambiado la forma en que los desarrolladores front-end conciben la creación de aplicaciones web. Con DOM virtual, React hace que las actualizaciones de la interfaz de usuario sean lo más eficientes posible, lo que hace que su aplicación web sea más ágil. Pero, ¿por qué las aplicaciones web React de tamaño moderado todavía tienden a funcionar mal?

Bueno, la clave está en cómo estás usando React.

Una biblioteca front-end moderna como React no hace que su aplicación sea más rápida por arte de magia. Requiere que el desarrollador comprenda cómo funciona React y cómo viven los componentes a través de las diversas fases del ciclo de vida del componente.

Con React, puede obtener muchas de las mejoras de rendimiento que tiene para ofrecer midiendo y optimizando cómo y cuándo se renderizan sus componentes. Y, React proporciona solo las herramientas y funcionalidades necesarias para hacerlo fácil.

Acelere su aplicación React optimizando el proceso de renderizado y diferenciación de sus componentes.

En este tutorial de React, aprenderá cómo puede medir el rendimiento de sus componentes de React y optimizarlos para crear una aplicación web de React con mucho más rendimiento. También aprenderá cómo algunas mejores prácticas de JavaScript también ayudan a hacer que su aplicación web React brinde una experiencia de usuario mucho más fluida.

¿Cómo funciona React?

Antes de sumergirnos en las técnicas de optimización, debemos tener una mejor comprensión de cómo funciona React.

En el núcleo del desarrollo de React, tiene la sintaxis JSX simple y obvia, y la capacidad de React para construir y comparar DOM virtuales. Desde su lanzamiento, React ha influido en muchas otras bibliotecas front-end. Las bibliotecas como Vue.js también se basan en la idea de los DOM virtuales.

Así es como funciona React:

Cada aplicación de React comienza con un componente raíz y se compone de muchos componentes en una formación de árbol. Los componentes en React son "funciones" que representan la interfaz de usuario en función de los datos (accesorios y estado) que recibe.

Podemos simbolizar esto como F .

 UI = F(data)

Los usuarios interactúan con la interfaz de usuario y hacen que los datos cambien. Ya sea que la interacción implique hacer clic en un botón, tocar una imagen, arrastrar elementos de la lista, solicitudes AJAX que invocan API, etc., todas esas interacciones solo cambian los datos. Nunca hacen que la interfaz de usuario cambie directamente.

Aquí, los datos son todo lo que define el estado de la aplicación web, y no solo lo que tiene almacenado en su base de datos. Incluso fragmentos de estados frontales (por ejemplo, qué pestaña está seleccionada actualmente o si una casilla de verificación está marcada actualmente) son parte de estos datos.

Cada vez que hay un cambio en estos datos, React usa las funciones del componente para volver a renderizar la interfaz de usuario, pero solo virtualmente:

 UI1 = F(data1) UI2 = F(data2)

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

 Changes = Diff(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 que React evite operaciones de manipulación DOM potencialmente costosas en el navegador, como crear nodos DOM y acceder a los existentes más allá de la necesidad.

Esta diferenciación y representación repetida de componentes puede ser una de las principales fuentes de problemas de rendimiento de React en cualquier aplicación de React. La creación de una aplicación React en la que el algoritmo de diferenciación no se reconcilia de manera efectiva, lo que hace que toda la aplicación se reproduzca repetidamente puede resultar en una experiencia frustrantemente lenta.

¿Por dónde empezar a optimizar?

Pero, ¿qué es exactamente lo que optimizamos?

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

Un DOM virtual de componentes React

Dada una parte de los cambios de datos, lo que queremos que haga React es volver a renderizar solo los componentes que se ven directamente afectados por el cambio (y posiblemente omitir incluso el proceso de diferencias para el resto de los componentes):

Reaccionar renderizando un número óptimo de componentes

Sin embargo, lo que React termina haciendo es:

Reaccionar desperdiciando recursos renderizando todos los componentes

En la imagen de arriba, todos los nodos amarillos se representan y diferencian, lo que resulta en una pérdida de tiempo/recursos informáticos. Aquí es donde pondremos principalmente nuestros esfuerzos de optimización. La configuración de cada componente para que solo represente las diferencias cuando sea necesario nos permitirá recuperar estos ciclos de CPU desperdiciados.

Los desarrolladores de la biblioteca React tomaron esto en consideración y nos proporcionaron un gancho para que hagamos precisamente eso: una función que nos permite decirle a React cuándo está bien omitir la representación de un componente.

Medir primero

Como Rob Pike lo expresa con bastante elegancia como una de sus reglas de programación:

La medida. No ajuste la velocidad hasta que haya medido, e incluso entonces no lo haga a menos que una parte del código supere al resto.

No comience a optimizar el código que crea que puede estar ralentizando su aplicación. En cambio, deje que las herramientas de medición de rendimiento de React lo guíen en el camino.

React tiene una poderosa herramienta solo para esto. Con la biblioteca react-addons-perf puede obtener una descripción general del rendimiento general de su aplicación.

El uso es muy simple:

 Import Perf from 'react-addons-perf' Perf.start(); // use the app Perf.stop(); Perf.printWasted();

Esto imprimirá una tabla con la cantidad de tiempo que se desperdician los componentes en el renderizado.

Tabla de componentes perdiendo tiempo en renderizar

La biblioteca proporciona otras funciones que le permiten imprimir diferentes aspectos del tiempo perdido por separado (por ejemplo, usando las printInclusive() o printExclusive() ), o incluso imprimir las operaciones de manipulación del DOM (usando la función printOperations() ).

Llevando el benchmarking un paso más allá

Si eres una persona visual, entonces react-perf-tool es justo lo que necesitas.

react-perf-tool se basa en la biblioteca react-addons-perf . Le brinda una forma más visual de depurar el rendimiento de su aplicación React. Utiliza la biblioteca subyacente para obtener medidas y luego las visualiza como gráficos.

Una visualización de componentes que pierden tiempo en renderizar

La mayoría de las veces, esta es una forma mucho más conveniente de detectar cuellos de botella. Puede usarlo fácilmente agregándolo como un componente a su aplicación.

¿Debería React actualizar el componente?

De forma predeterminada, React se ejecutará, renderizará el DOM virtual y comparará la diferencia de cada componente en el árbol para detectar cualquier cambio en sus accesorios o estado. Pero eso obviamente no es razonable.

A medida que su aplicación crezca, intentar volver a renderizar y comparar todo el DOM virtual en cada acción eventualmente se ralentizará.

React proporciona una forma sencilla para que el desarrollador indique si un componente necesita volver a renderizarse. Aquí es donde entra en juego el método shouldComponentUpdate .

 function shouldComponentUpdate(nextProps, nextState) { return true; }

Cuando esta función devuelve verdadero para cualquier componente, permite que se active el proceso render-diff.

Esto le brinda una forma simple de controlar el proceso de renderización y diferenciación. Siempre que necesite evitar que un componente se vuelva a renderizar, simplemente devuelva false desde la función. Dentro de la función, puede comparar el conjunto de accesorios y el estado actual y el siguiente para determinar si es necesario volver a renderizar:

 function shouldComponentUpdate(nextProps, nextState) { return nextProps.id !== this.props.id; }

Usando un React.PureComponent

Para facilitar y automatizar un poco esta técnica de optimización, React proporciona lo que se conoce como componente “puro”. Un React.PureComponent es exactamente como un React.Component que implementa una función shouldComponentUpdate() con una comparación superficial y de estado.

Un React.PureComponent es más o menos equivalente a esto:

 class MyComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return shallowCompare(this.props, nextProps) && shallowCompare(this.state, nextState); } … }

Como solo realiza una comparación superficial, puede que le resulte útil solo cuando:

  • Sus accesorios o estados contienen datos primitivos.
  • Sus accesorios y estados tienen datos complejos, pero sabe cuándo llamar a forceUpdate() para actualizar su componente.

Hacer que los datos sean inmutables

¿Qué pasaría si pudieras usar un React.PureComponent pero aún así tener una forma eficiente de saber cuándo cualquier accesorio o estado complejo ha cambiado automáticamente? Aquí es donde las estructuras de datos inmutables facilitan la vida.

La idea detrás del uso de estructuras de datos inmutables es simple. Siempre que cambie un objeto que contenga datos complejos, en lugar de realizar los cambios en ese objeto, cree una copia de ese objeto con los cambios. Esto hace que la detección de cambios en los datos sea tan simple como comparar la referencia de los dos objetos.

Puede usar Object.assign o _.extend (de Underscore.js o Lodash):

 const newValue2 = Object.assign({}, oldValue); const newValue2 = _.extend({}, oldValue);

Aún mejor, puede usar una biblioteca que proporcione estructuras de datos inmutables:

 var map1 = Immutable.Map({a:1, b:2, c:3}); var map2 = map1.set('b', 2); assert(map1.equals(map2) === true); var map3 = map1.set('b', 50); assert(map1.equals(map3) === false);

Aquí, Immutable.Map es proporcionado por la biblioteca Immutable.js.

Cada vez que se actualiza un mapa con su método set , se devuelve un nuevo mapa solo si la operación set cambió el valor subyacente. De lo contrario, se devuelve el mismo mapa.

Puede obtener más información sobre el uso de estructuras de datos inmutables aquí.

Más técnicas de optimización de la aplicación React

Uso de la compilación de producción

Al desarrollar una aplicación React, se le presentan advertencias y mensajes de error realmente útiles. Esto hace que la identificación de errores y problemas durante el desarrollo sea una bendición. Pero tienen un costo de rendimiento.

Si observa el código fuente de React, verá muchas comprobaciones de if (process.env.NODE_ENV != 'production') . Estos fragmentos de código que React está ejecutando en su entorno de desarrollo no son algo que necesite el usuario final. Para entornos de producción, todo este código innecesario se puede descartar.

Si arrancó su proyecto usando create-react-app , simplemente puede ejecutar npm run build para producir la compilación de producción sin este código adicional. Si usa Webpack directamente, puede ejecutar webpack -p (que es equivalente a webpack --optimize-minimize --define process.env.NODE_ENV="'production'" .

Funciones de enlace temprano

Es muy común ver funciones vinculadas al contexto del componente dentro de la función de representación. Este suele ser el caso cuando usamos estas funciones para manejar eventos de componentes secundarios.

 // Creates a new `handleUpload` function during each render() <TopBar onUpload={this.handleUpload.bind(this)} /> // ...as do inlined arrow functions <TopBar onUpload={files => this.handleUpload(files)} />

Esto hará que la función render() cree una nueva función en cada render. Una forma mucho mejor de hacer lo mismo es:

 class App extends React.Component { constructor(props) { super(props); this.handleUpload = this.handleUpload.bind(this); } render() { … <TopBar onUpload={this.handleUpload} /> … } }

Uso de varios archivos de fragmentos

Para las aplicaciones web React de una sola página, a menudo terminamos agrupando todo nuestro código JavaScript de front-end en un solo archivo minificado. Esto funciona bien para aplicaciones web de tamaño pequeño a moderado. Pero a medida que la aplicación comienza a crecer, la entrega de este archivo JavaScript incluido al navegador en sí mismo puede convertirse en un proceso lento.

Si está utilizando Webpack para crear su aplicación React, puede aprovechar sus capacidades de división de código para separar el código de su aplicación integrada en varios "fragmentos" y enviarlos al navegador según sea necesario.

Hay dos tipos de división: división de recursos y división de código bajo demanda.

Con la división de recursos, divide el contenido de los recursos en varios archivos. Por ejemplo, al usar CommonsChunkPlugin, puede extraer código común (como todas las bibliotecas externas) en un archivo "trozo" propio. Usando ExtractTextWebpackPlugin, puede extraer todo el código CSS en un archivo CSS separado.

Este tipo de división ayudará de dos maneras. Ayuda al navegador a almacenar en caché los recursos que cambian con menos frecuencia. También ayudará al navegador a aprovechar la descarga paralela para reducir potencialmente el tiempo de carga.

Una característica más notable de Webpack es la división de código bajo demanda. Puede usarlo para dividir el código en un fragmento que se puede cargar a pedido. Esto puede mantener pequeña la descarga inicial, lo que reduce el tiempo que lleva cargar la aplicación. Luego, el navegador puede descargar otros fragmentos de código a pedido cuando la aplicación lo necesite.

Puede obtener más información sobre la división de código de Webpack aquí.

Habilitación de Gzip en su servidor web

Los archivos JS del paquete de la aplicación React suelen ser muy grandes, por lo que para que la página web se cargue más rápido, podemos habilitar Gzip en el servidor web (Apache, Nginx, etc.)

Todos los navegadores modernos admiten y negocian automáticamente la compresión Gzip para solicitudes HTTP. Habilitar la compresión Gzip puede reducir el tamaño de la respuesta transferida hasta en un 90 %, lo que puede reducir significativamente la cantidad de tiempo para descargar el recurso, reducir el uso de datos para el cliente y mejorar el tiempo de procesamiento de sus páginas por primera vez.

Consulte la documentación de su servidor web sobre cómo habilitar la compresión:

  • Apache: usa mod_deflate
  • Nginx: use ngx_http_gzip_module

Usando Eslint-plugin-reaccionar

Debería usar ESLint para casi cualquier proyecto de JavaScript. Reaccionar no es diferente.

Con eslint-plugin-react , se obligará a adaptarse a muchas reglas en la programación de React que pueden beneficiar su código a largo plazo y evitar muchos problemas comunes y problemas que ocurren debido a un código mal escrito.

Haga que sus aplicaciones web React vuelvan a ser rápidas

Para aprovechar al máximo React, debe aprovechar sus herramientas y técnicas. El rendimiento de una aplicación web React radica en la simplicidad de sus componentes. Abrumar el algoritmo render-diff puede hacer que su aplicación funcione mal de manera frustrante.

Antes de que pueda optimizar su aplicación, deberá comprender cómo funcionan los componentes de React y cómo se representan en el navegador. Los métodos del ciclo de vida de React le brindan formas de evitar que su componente se vuelva a renderizar innecesariamente. Elimina esos cuellos de botella y tendrás el rendimiento de la aplicación que merecen tus usuarios.

Aunque hay más formas de optimizar una aplicación web React, ajustar los componentes para que se actualicen solo cuando sea necesario produce la mejor mejora en el rendimiento.

¿Cómo mide y optimiza el rendimiento de su aplicación web React? Compártelo en los comentarios debajo.

Relacionado: Recuperación de datos obsoletos mientras se revalida con ganchos de React: una guía