Video Summary and Transcription
Esta charla discute el rendimiento de React y cómo los re-renderizados pueden afectarlo. Destaca errores comunes y conceptos erróneos, como el uso excesivo de los hooks useMemo y useCallback. Se enfatiza la importancia de React.memo en la prevención de re-renderizados innecesarios de componentes hijos. La creación de componentes en funciones de renderizado se identifica como un gran asesino del rendimiento, y se explican los beneficios de mover el estado hacia abajo y envolver el estado alrededor de los hijos. La charla también cubre la optimización del renderizado de componentes a través de la memorización y proporciona un resumen de los puntos clave.
1. Introducción al rendimiento de React y las re-renderizaciones
Hola a todos. Mi nombre es Nadia. Soy una arquitecta de front-end, programadora y escritora. He trabajado en Atlassian y ahora soy ingeniera fundadora en una startup llamada PIN. Hoy, quiero compartir mis conocimientos sobre el rendimiento de React y cómo las re-renderizaciones lo afectan. Una re-renderización es cuando un componente actualiza sus datos. Hay tres formas de desencadenar una re-renderización: cambio de estado o props, cambio de valor de contexto y re-renderización del componente padre. Las re-renderizaciones innecesarias pueden ralentizar la aplicación y deben evitarse.
Hola a todos. Mi nombre es Nadia. Entonces, primero una pequeña introducción. Soy una arquitecta de front-end. Soy programadora. Soy escritora. Trabajé en Atlassian durante algunos años. Así que trabajé en el front-end de Jira, y ahora soy una ingeniera fundadora en una pequeña startup que se llama PIN en Australia.
Entonces, el tema del rendimiento de React y especialmente cómo las re-renderizaciones afectan el rendimiento de React es algo así como una pasión mía. Me parece fascinante que solo un pequeño cambio en el lugar correcto puede destruir completamente o mejorar enormemente el rendimiento de una gran aplicación. Entonces, todo el conocimiento sobre este tema es lo que quiero compartir con ustedes hoy.
Pero primero, ¿qué es exactamente una re-renderización y por qué queremos hablar de re-renderizaciones en el contexto del rendimiento. En general, tenemos dos etapas principales del ciclo de vida de React que necesitamos cuidar. La primera es la renderización inicial cuando una aplicación se monta por primera vez y aparece en la pantalla, y luego la re-renderización. Una re-renderización es la segunda y todas ellas, todas las renderizaciones consecutivas de una aplicación que ya está en la pantalla. Y desde una perspectiva de código, tenemos al menos tres formas de desencadenar una re-renderización de un componente. La primera es la más conocida, es cuando un estado o props cambian, es cuando un componente será re-renderizado. La segunda es si usamos un contexto, entonces cuando un valor cambia, cada componente que usa este valor también se re-renderizará. Y la tercera y más subestimada es cuando un componente padre se re-renderiza o si miramos desde arriba, eso significa que cuando un componente se re-renderiza a sí mismo, re-renderizará a cada uno de sus hijos. Si queremos visualizar el último, porque es el más importante, se verá algo así. Tenemos un árbol de componentes, el de arriba se re-renderizará, y luego esta re-renderización desencadenará una re-renderización de todos los hijos allí, y luego todos los hijos debajo, por lo que será una cadena completa de re-renderizaciones que se desencadenan desde arriba.
Y en general, una re-renderización en sí no es algo con lo que querríamos luchar porque es una parte esencial del ciclo de vida de React, es cuando React actualiza todos los datos que han cambiado. Lo que queremos evitar a toda costa son las re-renderizaciones innecesarias. Y por re-renderización innecesaria, me refiero a algo así. Así que imagina que tenemos un componente de entrada en algún lugar en la parte inferior, escribimos algo allí, y entonces este componente naturalmente se re-renderizará a sí mismo. Eso está bien, y eso es lo esperado. Lo que no queremos es que cuando escribimos en este pequeño componente de entrada, se re-renderice toda la aplicación. Esto, dependiendo del tamaño de la aplicación, puede ser extremadamente lento. Y en el mundo de hoy, los usuarios esperarán que todas las interacciones en la página sean realmente, realmente rápidas. Por lo tanto, las re-renderizaciones innecesarias son un asesino del rendimiento.
2. Errores de rendimiento de React y Hooks inútiles
Y para demostrar lo malo que puede ser el rendimiento, implementé una aplicación que renderiza una lista de componentes. Los errores en el código hicieron que la aplicación fuera insoportablemente lenta. Un error común es el mito de useMemo y useCallback, que lleva a una aplicación llena de hooks useMemo y useCallback. Sin embargo, memorizar todo puede hacer que la aplicación sea incomprensible y no depurable. Además, envolver onClick en useCallback puede ser inútil porque los componentes hijos aún pueden volver a renderizarse cuando el componente padre se vuelve a renderizar.
Y para demostrarles lo malo que pueden ser, incluso implementé un poco de una aplicación. Así que esta es una aplicación que renderiza una lista de componentes, y tiene un poco de interactividad. Así que echen un vistazo. A la derecha, la pestaña Performance, hago clic en todas partes, y todo es instantáneo. A la izquierda, exactamente la misma aplicación, pero cometí un par de errores allí, y miren lo insoportablemente lenta que es todo esto. Solo unos pocos errores pequeños en los lugares correctos, y acabo de destruir esta aplicación. Y es solo una lista de componentes.
Entonces, los errores comunes que conducen a un rendimiento como ese, y también útiles consejos de rendimiento tips y trucos para evitar re-renderizaciones de toda la aplicación, es lo que quiero compartir con ustedes hoy. Comencemos con los errores. El primero es uno de mis favoritos, es lo que llamo el mito de useMemo y useCallback. Así como probablemente la mayoría de ustedes saben, React utiliza igualdad referencial cuando compara props o dependencias en todos los diversos hooks. Y la igualdad referencial es básicamente esto. Tenemos dos arrays o dos objetos. Si queremos compararlos, si lo hacemos así, el resultado será falso, porque estamos comparándolos por la referencia, no por el valor real.
Y desde la perspectiva de React, suena así. Tenemos un componente, renderiza un componente hijo. Paso un valor a este componente hijo que es un array. Si lo hago así, durante una re-renderización, este valor se convertirá en un valor completamente diferente. Entonces, si React compara esas props, React pensará que el valor de la prop ha cambiado. Y si escribo hooks memo y uso callback, hooks que te permiten memorizar este valor, y básicamente para preservar la referencia a este valor entre re-renderizaciones. Entonces, si extraigo este array en el hook useMemo, entonces cuando ocurre una re-renderización, React pensará que el valor en un componente hijo será exactamente el mismo. Y el hecho de que una de las razones más importantes por las que un componente se re-renderiza es un cambio de estado o prop, en combinación con cómo funcionan esos hooks, lleva a la creencia generalizada de que si memorizamos todas las props en un componente, eso evitará que este componente se re-renderice. Y esto resulta en algo que llamo un infierno de hooks useMemo o useCallback, porque memorizar absolutamente todo lleva a que tu aplicación se convierta, teniendo useMemo, envuelves en useCallback, y luego otro useCallback, es solo useMemo y useCallbacks en todas partes, y la aplicación se vuelve incomprensible y completamente ilegible y no depurable. Así que creo que estos se vuelven realmente horribles.
Pero la peor parte de todo esto es que en realidad a veces, es inútil, porque estamos olvidando un componente clave en toda esta construcción. Entonces, si echamos un vistazo, por ejemplo, a este code, vemos un componente, tiene un componente hijo y luego onClick, Prop, y queremos evitar que el componente hijo se re-renderice envolviendo onClick en un hook useCallback. Pero, ¿qué puede exactamente desencadenar que los componentes hijos se re-rendericen? Evitamos los cambios de Prop. Lo único que queda es cuando un componente padre se re-renderiza. Entonces, activaremos un estado, por ejemplo, un componente hijo se re-renderizará, y React en realidad no comprobará si Prop ha cambiado o no en esta etapa, porque la forma natural de React de tratar con los componentes es que los componentes se re-renderizan, y luego re-renderizan cada uno de los hijos. Envolver onClick aquí en useCallback es simplemente completamente inútil, no estamos haciendo nada aquí.
3. Previniendo la Re-renderización de Componentes Hijos
La única forma de prevenir que el componente hijo se re-renderice cuando el componente padre se re-renderiza es envolviéndolo en React.memo. Y solo en este escenario, cuando un componente padre se re-renderiza, se detendrá la re-renderización de este hijo. En este caso, el componente React se detendrá, y luego comenzaremos a comparar todas las Props con el valor anterior, y si todas son iguales, entonces nada se re-renderizará.
La única forma de prevenir que el componente hijo se re-renderice cuando el componente padre se re-renderiza es envolviéndolo en React.memo. Y solo en este escenario, cuando un componente padre se re-renderiza, la re-renderización de este hijo se detendrá. En este caso, el componente react se detendrá, y luego comenzaremos a comparar todas las Props con el valor anterior, y si todas son iguales, entonces nada se re-renderizará. Entonces, desde la perspectiva de code, esto, cuando solo tenemos el componente hijo, y luego un useCallback que es, envuelve una Prop, es inútil. No hace nada. Solo consume un poco de poder computacional. Debería ser algo así, o simplemente recordar useCallback. envuelto en React.memo, y entonces, y solo entonces, un useCallback será realmente útil.
4. Creando Componentes en Funciones de Renderizado
Crear componentes en funciones de renderizado es un gran asesino del rendimiento en las aplicaciones de React. Cuando los componentes se crean dentro de un componente padre, React los recrea desde cero durante cada re-renderizado del componente padre. Esto no solo ralentiza la aplicación, sino que también causa destellos visibles en la pantalla e introduce errores con el enfoque. La solución es crear componentes de elementos fuera de los componentes grandes y evitar crear componentes en línea.
Segundo error, y este es el mayor asesino del performance en las aplicaciones de React. Creando componentes en funciones de renderizado. De nuevo, recuerda la aplicación. La aplicación es solo una lista. Las aplicaciones, acepta países como una prop, y luego iteramos sobre esos países y renderizamos algo. En la vida real, obviamente querríamos que este botón tenga algunos estilos, tenga alguna funcionalidad, así que en la vida real querría extraer este botón y convertirlo en un componente. Y lo que la gente a menudo hace es que lo extrae como un componente y lo crea dentro de este exacto componente, lo crea dentro de un componente padre. Por lo general, la razón de esto es que es mucho más fácil pasar data adicional a él que se deriva del estado, pero aún así. Pero cuando hacemos algo así, durante cada re-renderizado del componente de lista grande, React recreará el componente de elemento completamente desde cero. React simplemente desmontará todo lo que ya está renderizado y luego volverá a montar todos esos componentes de lista. Esto no solo va a ser realmente, realmente lento, el montaje es dos veces más lento que solo un renderizado. También será visible en la pantalla, porque lo que sucederá es un componente de lista con elementos, listas de renderizados. React pensará que todo el elemento creado dentro de la lista es un nuevo componente ahora, por lo que destruirá todos esos elementos, los eliminará de la pantalla y luego los recreará. A veces veremos un destello visible en la pantalla. Y también numerosos errores con el enfoque. Entonces, la forma de solucionar esto, por supuesto, es simplemente nunca hacer algo así, y simplemente crear componentes de elementos fuera de los componentes grandes, nunca crear componentes en línea.
5. Proveedor de Contexto y Moviendo el Estado Hacia Abajo
El tercer error que causa renders de error espontáneos e imperceptibles es el proveedor de contexto. Cuando un valor de contexto cambia, React necesita volver a renderizar cada componente que usa ese valor. Para solucionar esto, siempre use memo en el valor del contexto. Otro truco para prevenir rerenders innecesarios es el patrón llamado Moviendo el Estado Hacia Abajo. Al extraer el estado en un componente separado, solo los hijos necesarios volverán a renderizar cuando se actualice el estado.
El tercer error que probablemente es la fuente más importante de todos esos renders de error espontáneos e imperceptibles en toda la aplicación es el proveedor de contexto. Entonces, el contexto es una herramienta realmente útil cuando necesitamos pasar algunos data y evitar el trilling de props. Entonces, con el contexto, podemos hacer esto. Podemos simplemente escapar de todos los componentes intermedios y pasar data desde el componente superior al componente inferior. Sin contexto, lo que tendríamos que hacer es pasar data a través de cada componente individual entre el superior y el inferior, lo que hará que todos los componentes intermedios exploten con props y data innecesarios y se conviertan en una pesadilla de refactorización en seis meses.
Pero hay una advertencia con el contexto. Cuando un valor de contexto cambia, React también necesitará actualizar todo lo que está usando el valor del contexto. Y eso significa que React necesita volver a renderizar cada componente que usa el valor del contexto. Y desde una perspectiva de codificación, se ve algo así. Tenemos un componente, tenemos un proveedor de contexto, y necesitamos pasar un valor allí. ¿Qué sucederá si este componente se vuelve a renderizar por alguna razón? Recuerda, igualdad diferencial. Tenemos un objeto que pasamos al proveedor de contexto. El componente se vuelve a renderizar. React piensa que el objeto es un objeto diferente. Cambia, piensa que un valor cambia, y luego volverá a renderizar innecesariamente cada componente individual que usa este contexto. Y la forma de solucionarlo sería simplemente usar memo en este valor siempre. Diría que este es uno de los casos muy prematuros de optimization que realmente quieres, porque debugging rerenders que están sucediendo debido a los cambios en el contexto es simplemente una pesadilla para hacer.
Bueno. Entonces, dejemos de deprimirnos por los errores y hablemos un poco sobre los trucos reales. Uno de los más importantes en tu arsenal contra los rerenders innecesarios es el patrón que se llama Moviendo el Estado Hacia Abajo. Entonces, si miramos el code, tenemos un componente, renderiza muchas cosas, es muy pesado, y en algún momento implementamos un botón allí, hacemos clic en el botón, se abre un diálogo de modelo. Funcionalidad súper simple esta implementación completa. Entonces, ¿qué sucederá en este gran componente desde la perspectiva de rerender? Hacemos clic en un botón, actualizamos el estado, y luego todo esto se vuelve a renderizar, porque cuando se actualiza el estado, cada hijo en el componente se volverá a renderizar. Y por supuesto, algo como esto va a ser realmente, realmente lento. Si es una aplicación realmente grande, hacer clic en un botón y luego abrir un diálogo de modelo podría causar un retraso visible, porque React necesita volver a renderizar todo primero antes de abrir realmente el diálogo. Nuestros usuarios estarán muy decepcionados. La forma de solucionarlo es lo que se llama moviendo el estado hacia abajo. Este estado está bastante separado de la funcionalidad real de esta gran aplicación, entonces, lo que podemos hacer es simplemente extraer todo esto y envolverlo en un componente en sí. Y luego usarlo de nuevo en este gran componente. Entonces, ahora desde la perspectiva de ReRender, ¿qué sucederá? Hacemos clic en un botón, se actualiza el estado, los hijos se vuelven a renderizar, pero en este caso los hijos son solo un botón y un diálogo de modelo.
6. Optimizando la Renderización de Componentes
El gran componente simplemente se quedará allí y no hará nada. Eso es exactamente lo que queremos. Y la apertura del diálogo será ahora lo más rápida posible. Envolver el estado alrededor de los hijos es otro patrón que está infravalorado y es el menos conocido. Es similar a mover el estado hacia abajo. Al escuchar un evento de desplazamiento, podemos extraer el estado fuera del div envolvente y crear un nuevo componente. De esta manera, el componente hijo no se vuelve a renderizar, haciendo la aplicación más rápida. Otra técnica es memorizar parte de la secuencia de renderización, donde un gran fragmento de la secuencia que no depende del estado puede ser memorizado.
El gran componente simplemente se quedará allí y no hará nada. Eso es exactamente lo que queremos. Y la apertura del diálogo será ahora lo más rápida posible. El segundo patrón y probablemente este es el patrón más infravalorado de la serie y también el menos conocido. Eso es envolver el estado alrededor de los hijos. Es un poco similar a mover el estado hacia abajo. Entonces, de nuevo, tenemos un componente. En este caso queremos, por ejemplo, escuchar un evento de desplazamiento. Entonces, lo que haremos aquí, desde una perspectiva de re-renderización de nuevo, el usuario se desplaza. Estamos activando actualizaciones de estado. Se activa la actualización. Eso provoca una re-renderización de todo el gran componente. Y de nuevo, todo se vuelve a renderizar. Pero en este caso, no podemos simplemente mover el estado fuera porque este div está realmente envolviendo todo el componente. Pero lo que podemos hacer es que aún podemos extraerlo fuera, crear un nuevo componente de todo esto, y luego pasar todo lo que estaba entre esos divs como hijos.
¿Qué sucederá aquí desde una perspectiva de re-renderización y cómo usarlo? Entonces, ahora, nos desplazamos. Activamos la actualización del estado. Se activa la actualización. Provoca re-renderizaciones. El componente de desplazamiento se vuelve a renderizar a sí mismo. Pero todo lo que está dentro de aquí pertenece al padre. El componente hijo no sabe nada de todo esto. Desde la perspectiva del componente de desplazamiento, todo esto es solo una prop. Todo esto no se volverá a renderizar. Ahora esta aplicación se vuelve lo más rápida posible de nuevo, sin ninguna memorización, sin nada.
Y por último, pero no menos importante, memorizando parte de la secuencia de renderización. Entonces, cuando tienes un gran componente, usas un poco de estado aquí y allá. Pero no puedes mover un estado hacia abajo ni simplemente envolverlo alrededor de los hijos, porque está simplemente disperso por la aplicación. Pero también, si quieres mejorar el performance de esta aplicación un poco, y tienes un gran fragmento de la secuencia que no depende del estado, lo que puedes hacer es simplemente memorizar toda esta secuencia. En este caso, de nuevo, se activa la actualización, se activa la re-renderización, no hay dependencia en este memo, así que useMemo simplemente devolverá exactamente el mismo valor que había antes.
Resumen de Rendimiento de React y Preguntas y Respuestas
No actualizará nada. Siempre memoriza los valores en el proveedor de contexto. Envolver el estado hacia abajo y envolver el estado alrededor de los hijos son las herramientas más importantes en tu lucha contra las re-renderizaciones innecesarias. Si ambos no son posibles por alguna razón, también puedes memorizar partes costosas de la cadena de renderización. Si quieres leer un poco más sobre todo esto, o jugar con la mala aplicación y la buena aplicación, aquí están los enlaces. Siéntete libre de hacerlo. Pasemos a algunas preguntas. ¿Qué sucederá si pasamos el objeto a un componente hijo, que se exporta con React memo? Si un componente hijo está envuelto en React memo, y simplemente lo estamos renderizando, entonces necesitamos memorizar este objeto. De lo contrario, si el objeto no está memorizado, el componente que está envuelto en React memo seguirá siendo renderizado.
No actualizará nada. Y el performance de esta gran aplicación todavía será mucho mejor que sin esto.
Eso fue mucha información, espero que hayas encontrado al menos algo útil. Un pequeño resumen para todos. Si no estás usando useMemo y useCallbacks de la manera correcta, que es atrapar el componente en react.memo y memorizar cada uno de ellos, se vuelven inútiles. Puedes simplemente eliminarlos. Crear componentes dentro de las funciones de renderizado es el mayor asesino del performance. Nunca lo hagas. Debería ser tu mantra. Siempre memoriza los valores en el proveedor de contexto. Eso evitará que los consumidores de contexto de React se vuelvan a renderizar innecesariamente. Envolver el estado hacia abajo y envolver el estado alrededor de los hijos son las herramientas más importantes en tu lucha contra las re-renderizaciones innecesarias. Si ambos no son posibles por alguna razón, también puedes memorizar partes costosas de la cadena de renderización.
Si quieres leer un poco más sobre todo esto, o jugar con la mala aplicación y la buena aplicación, aquí están los enlaces. Siéntete libre de hacerlo. Escribo mucho sobre todas estas cosas y todos los diferentes patterns en mi blog. Siéntete libre de conectarte conmigo en Twitter o LinkedIn. Siempre feliz de charlar y responder preguntas. ¡Gracias! Muchas gracias Nadia, fue una charla realmente increíble. Tuve que tomar una foto de esa diapositiva al final, porque hay algunas de esas conclusiones que quiero asegurarme de que estoy usando yo mismo.
Pasemos a algunas preguntas. Si estás en la audiencia y tal vez te estás yendo rápidamente a otra charla, eso es totalmente aceptable. Mantengamos el ruido bajo para que podamos escuchar todas estas preguntas, incluso en la parte de atrás. Bien, vamos directamente a ello. ¿Qué sucederá si pasamos el objeto a un componente hijo, que se exporta con React memo? No estoy seguro si he leído eso correctamente. Si un componente hijo está envuelto en React memo, supongo, y simplemente lo estamos renderizando entonces necesitamos memorizar este objeto. De lo contrario, si el objeto no está memorizado, el componente que está envuelto en React memo seguirá siendo renderizado. Porque React piensa que las props son diferentes, por lo que se activará una renderización. Eso tiene sentido. Eso tiene sentido.
React memo y Renderizado de Hijos
React memo con el hijo no es el comportamiento predeterminado para renderizar hijos. Memorizar todo puede hacer que la aplicación sea un poco más lenta, aumentando el renderizado inicial. Puede o no salvarte de las re-renderizaciones.
Y una cosa, esto es también algo que se me ocurrió. ¿Por qué React memo con el hijo no es simplemente el comportamiento predeterminado para renderizar hijos? Porque tiene mucho sentido usarlo de esa manera. Tiene mucho sentido. Pero hasta donde yo sé, el equipo de React, no soy parte de él, así que solo uso internet. Experimentaron con hacer exactamente eso durante la construcción. Pero simplemente memorizar todo también tiene sus costos. Entonces, si simplemente memorizas todo, podría hacer que tu aplicación sea un poco más lenta. Porque de nuevo, si todo está envuelto en React memo, pero no estás envolviendo todo en el useMemo y useCallback, esos son inútiles, pero aún estás haciendo cosas durante eso. Por lo tanto, puede aumentar tu renderizado inicial. Puede o no salvarte de las re-renderizaciones, pero definitivamente aumentará tu inicial renderizado. También tiene sentido. Y otra que tenemos, ¿está bien renderizar un componente incluso si no es visible o abierto por defecto? Bueno, depende. En mi cabeza, también estaba pensando que a veces podemos y a veces no podemos, pero ¿tienes algún tipo de pensamientos o perspectiva sobre la respuesta? Por ejemplo, para diálogos de modelo, si quieres hacerlo como una transición suave que lentamente aparece, tendrías que renderizarlo un poco antes. Entonces está ahí pero invisible y luego aparece. Así que este es el caso en el que querrías renderizarlo, pero aún es invisible. Eso tiene sentido. Eso tiene sentido. Creo que eso es todo lo que tenemos de preguntas. ¿Hay alguna más? ¿Quizás hay algunas que me perdí? Asegúrate de seguir enviando tus preguntas. Puedes unirte en el Slido en 1721. Y si tienes más preguntas también, tal vez quieras profundizar, puedes encontrar a Nadia en la sala de discussion de los oradores justo a la vuelta de la esquina. Muy bien, gente, muchas gracias, Nadia. Demosle otra ronda de aplausos.
Comments