Junto con la vida, la muerte, el destino y los impuestos, el comportamiento de reproducción de React es una de las mayores verdades y misterios de la vida.

¡Vamos a bucear!

Como todos los demás, comencé mi viaje de desarrollo de front-end con jQuery. La manipulación de DOM basada en JS pura era una pesadilla en ese entonces, por lo que era lo que todos estaban haciendo. Luego, lentamente, Frameworks basados ​​en JavaScript se volvió tan prominente que ya no podía ignorarlos.

El primero que aprendí fue Vista. Lo pasé increíblemente difícil porque los componentes, el estado y todo lo demás era un modelo mental totalmente nuevo, y fue mucho dolor encajar todo. Pero finalmente, lo hice y me doy una palmada en la espalda. Felicitaciones, amigo, me dije a mí mismo, has hecho la empinada subida; ahora, el resto de los marcos, si alguna vez necesita aprenderlos, será muy fácil.

Entonces, un día, cuando comencé aprendizaje Reaccionar, Me di cuenta de lo terriblemente equivocado que estaba. Facebook no facilitó las cosas al lanzar Hooks y decirles a todos: “Oigan, usen esto de ahora en adelante. Pero no reescriba las clases; las clases están bien. En realidad, no tanto, pero está bien. Pero los Hooks lo son todo y son el futuro.

¿Entendido? ¡Excelente!".

Finalmente, también crucé esa montaña. Pero luego me golpeó algo tan importante y difícil como el propio React: representación.

¡¡¡Sorpresa!!!

Si te has encontrado con el renderizado y sus misterios en React, sabes de lo que estoy hablando. Y si no lo ha hecho, ¡no tiene idea de lo que le espera! 😂

Pero antes de perder el tiempo en algo, es un buen hábito preguntar qué ganarías con eso (a diferencia de mí, que soy un idiota sobreexcitado y felizmente aprenderá cualquier cosa por el simple hecho de hacerlo 😭😭). Si su vida como desarrollador de React va bien sin preocuparse por lo que es esta representación, ¿por qué preocuparse? Buena pregunta, así que respondamos esto primero, y luego veremos qué es realmente el renderizado.

Why is understanding rendering behavior in React important?

Todos comenzamos a aprender React escribiendo componentes (en estos días, funcionales) que devuelven algo llamado JSX. También entendemos que este JSX se convierte de alguna manera en elementos DOM HTML reales que aparecen en la página. Las páginas se actualizan a medida que se actualiza el estado, las rutas cambian como se esperaba y todo está bien. Pero esta visión de cómo funciona React es ingenua y una fuente de muchos problemas.

Si bien a menudo logramos escribir aplicaciones completas basadas en React, hay ocasiones en las que encontramos ciertas partes de nuestra aplicación (o la aplicación completa) notablemente lentas. Y la peor parte. . . ¡No tenemos ni idea de por qué! Hemos hecho todo correctamente, no vemos errores ni advertencias, hemos seguido todas las buenas prácticas de diseño de componentes, estándares de codificación, etc., y no hay lentitud en la red ni costosos cálculos de lógica empresarial entre bastidores. 🤔

A veces, es un problema totalmente diferente: no hay nada de malo en el rendimiento, pero la aplicación se comporta de manera extraña. Por ejemplo, realizar tres llamadas a la API al backend de autenticación, pero solo una a todos los demás. O algunas páginas se vuelven a dibujar dos veces, con la transición visible entre los dos renders de la misma página creando una UX discordante.

¡Oh no! ¡¡No otra vez!!

Lo peor de todo es que no hay ayuda externa disponible en casos como estos. Si vas a tu foro de desarrollo favorito y haces esta pregunta, te responderán: “No puedo saberlo sin mirar tu aplicación. ¿Puede adjuntar un ejemplo de trabajo mínimo aquí? " Bueno, por supuesto, no puede adjuntar la aplicación completa por razones legales, mientras que un pequeño ejemplo funcional de esa parte puede no contener ese problema porque no interactúa con todo el sistema de la forma en que lo está en la aplicación real.

¿Atornillado? Sí, si me preguntas. 🤭🤭

Así que, a menos que desee ver esos días de aflicción, le sugiero que desarrolle una comprensión y un interés, debo insistir; la comprensión adquirida a regañadientes no lo llevará muy lejos en el mundo de React, en esta cosa mal entendida llamada renderizado en React. Créame, no es tan difícil de entender, y aunque es muy difícil de dominar, llegará muy lejos sin tener que conocer todos los rincones.

What does rendering mean in React?

Esa, amigo mío, es una excelente pregunta. No solemos preguntarlo cuando aprendemos React (lo sé porque no lo hice) porque la palabra "render" quizás nos adormezca con un falso sentido de familiaridad. Si bien el significado del diccionario es completamente diferente (y no es importante en esta discusión), los programadores ya tenemos una noción de lo que debería significar. Trabajar con pantallas, API 3D, tarjetas gráficas y leer especificaciones de productos entrena nuestras mentes para pensar en algo como "pintar una imagen" cuando leemos la palabra "render". En la programación del motor de juegos, hay un Renderer, cuyo único trabajo es, ¡precisamente !, pintar el mundo como lo entrega la Escena.

Y entonces pensamos que cuando React "renderiza" algo, recopila todos los componentes y vuelve a pintar el DOM de la página web. Pero en el mundo de React (y sí, incluso en la documentación oficial), de eso no se trata el renderizado. Por lo tanto, apretémonos los cinturones de seguridad y sumergámonos en lo más profundo de los componentes internos de React.

"Voy a ser condenado . . . "

Debe haber escuchado que React mantiene lo que se llama un DOM virtual y que periódicamente lo compara con el DOM real y aplica los cambios según sea necesario (es por eso que no puede simplemente agregar jQuery y React juntos; React necesita tomar el control total de el DOM). Ahora, este DOM virtual no se compone de elementos HTML como lo hace el DOM real, sino de elementos React. ¿Cual es la diferencia? ¡Buena pregunta! ¿Por qué no crear una pequeña aplicación React y comprobarlo por nosotros mismos?

yo creé este aplicación React muy simple para este propósito. El código completo es solo un archivo que contiene algunas líneas:

import React from "react";
import "./styles.css";

export default function App() {
  const element = (
    <div className="App">
      <h1>Hello, there!</h1>
      <h2>Let's take a look inside React elements</h2>
    </div>
  );

  console.log(element);
  return element;
}

¿Se da cuenta de lo que estamos haciendo aquí?

Sí, simplemente registrando cómo se ve un elemento JSX. Estas expresiones y componentes JSX son algo que hemos escrito cientos de veces, pero rara vez prestamos atención a lo que está sucediendo. Si abre la consola de desarrollo de su navegador y ejecuta esta aplicación, verá una Object que se expande a:

Esto puede parecer intimidante, pero tome nota de algunos detalles interesantes:

  • Lo que estamos viendo es un objeto JavaScript normal y simple y no un nodo DOM.
  • Observe que la propiedad props dice que tiene un className of App (que es la clase CSS establecida en el código) y que este elemento tiene dos elementos secundarios (esto también coincide, los elementos secundarios son los <h1> y <h2> etiquetas).
  • El proceso de la _source La propiedad nos dice dónde comienza el código fuente el cuerpo del elemento. Como puede ver, nombra el archivo App.js como fuente y menciona la línea número 6. Si vuelve a mirar el código, encontrará que la línea 6 está justo después de la etiqueta JSX de apertura, lo cual tiene sentido. Los paréntesis JSX que no contengo el elemento React; no son parte de ella, ya que sirven para transformarse en un React.createElement() llama más tarde.
  • El proceso de la __proto__ La propiedad nos dice que este objeto deriva todos sus. propiedades del JavaScript raíz Object, lo que refuerza nuevamente la idea de que solo estamos viendo objetos de JavaScript cotidianos aquí.

Entonces, ahora, entendemos que el llamado DOM virtual no se parece en nada al DOM real, sino que es un árbol de objetos React (JavaScript) que representan la interfaz de usuario en ese momento.

* SUSPIRO *. . . ¿Ya llegamos?

¿Agotado?

Créeme, yo también. 🙂 Dar vueltas a estas ideas una y otra vez en mi cabeza para tratar de presentarlas de la mejor manera posible, y luego pensar en las palabras para sacarlas a relucir y reorganizarlas, no es fácil. 😫

¡Pero nos estamos distrayendo!

Habiendo sobrevivido hasta aquí, ahora estamos en condiciones de responder la pregunta que buscábamos: ¿qué es el renderizado en React?

Bueno, el renderizado es el proceso del motor React que recorre el DOM virtual y recopila el estado actual, los accesorios, la estructura, los cambios deseados en la interfaz de usuario, etc. React ahora actualiza el DOM virtual usando algunos cálculos y también compara el nuevo resultado con el DOM real en la pagina. Este cálculo y comparación es lo que el equipo de React llama oficialmente "reconciliación", y si está interesado en sus ideas y algoritmos relevantes, puede consultar el informe oficial. documentos.

Time to Commit!

Una vez finalizada la parte de renderizado, React inicia una fase llamada "commit", durante la cual aplica los cambios necesarios al DOM. Estos cambios se aplican de forma sincrónica (uno tras otro, aunque se espera pronto un nuevo modo que funcione simultáneamente) y el DOM se actualiza. Exactamente cuándo y cómo React aplica estos cambios no es nuestra preocupación, ya que es algo que está totalmente oculto y es probable que siga cambiando a medida que el equipo de React pruebe cosas nuevas.

Rendering and performance in React apps

A estas alturas hemos entendido que renderizar significa recopilar información, y no es necesario que genere cambios de DOM visual cada vez. También sabemos que lo que consideramos "renderizado" es un proceso de dos pasos que implica renderizar y confirmar. Ahora veremos cómo se activa el renderizado (y lo que es más importante, el re-renderizado) en las aplicaciones React y cómo no conocer los detalles puede hacer que las aplicaciones funcionen mal.

Volver a renderizar debido a un cambio en el componente principal

Si un componente padre en React cambia (digamos, porque su estado o accesorios cambiaron), React recorre todo el árbol por este elemento padre y vuelve a renderizar todos los componentes. Si su aplicación tiene muchos componentes anidados y muchas interacciones, sin saberlo, está sufriendo un gran impacto en el rendimiento cada vez que cambia el componente principal (asumiendo que es solo el componente principal que desea cambiar).

Es cierto que el renderizado no hará que React cambie el DOM real porque, durante la reconciliación, detectará que nada ha cambiado para estos componentes. Pero, todavía es tiempo de CPU y memoria desperdiciada, y se sorprenderá de lo rápido que se acumula.

Reproducción debido a un cambio de contexto

La función Context de React parece ser la herramienta de administración de estado favorita de todos (algo para lo que no fue construida en absoluto). Todo es muy conveniente: simplemente envuelva el componente superior en el proveedor de contexto, ¡y el resto es muy sencillo! La mayoría de las aplicaciones de React se están construyendo así, pero si ha leído este artículo hasta ahora, probablemente haya detectado lo que está mal. Sí, cada vez que se actualiza el objeto de contexto, desencadena una nueva representación masiva de todos los componentes del árbol.

La mayoría de las aplicaciones no tienen conciencia de rendimiento, por lo que nadie se da cuenta, pero como se dijo antes, tales descuidos pueden ser muy costosos en aplicaciones de alto volumen y alta interacción.

Improving React rendering performance

Entonces, dado todo esto, ¿qué podemos hacer para mejorar el rendimiento de nuestras aplicaciones? Resulta que hay algunas cosas que podemos hacer, pero tenga en cuenta que solo discutiremos en el contexto de los componentes funcionales. Los componentes basados ​​en clases están muy desaconsejados por el equipo de React y están a punto de desaparecer.

Utilice Redux o bibliotecas similares para la gestión del estado

Aquellos que aman el mundo rápido y sucio de Context tienden a odiar Redux, pero esta cosa es muy popular por buenas razones. Y una de estas razones es el rendimiento: el connect() La función en Redux es mágica ya que (casi siempre) representa correctamente solo aquellos componentes según sea necesario. Sí, simplemente siga la arquitectura estándar de Redux y el rendimiento es gratis. No es una exageración en absoluto que si adopta la arquitectura Redux, evita la mayoría de los problemas de rendimiento (y otros) de inmediato.

Utiliza memo() para "congelar" componentes

El nombre "memo" proviene de Memoization, que es un nombre elegante para el almacenamiento en caché. Y si no ha encontrado mucho almacenamiento en caché, está bien; aquí hay una descripción diluida: cada vez que necesita algún resultado de cálculo / operación, mira en el lugar donde ha estado manteniendo los resultados anteriores; si lo encuentra, genial, simplemente devuelva ese resultado; si no, continúe y realice esa operación / cálculo.

Antes de sumergirse directamente en memo(), primero veamos cómo ocurre la renderización innecesaria en React. Comenzamos con un escenario sencillo: una pequeña parte de la interfaz de usuario de la aplicación que muestra al usuario cuántas veces le ha gustado el servicio / producto (si tiene problemas para aceptar el caso de uso, piense en cómo en Medium puede "aplaudir ”Varias veces para mostrar cuánto apoyas / me gusta un artículo).

También hay un botón que les permite aumentar los Me gusta en 1. Y finalmente, hay otro componente dentro que muestra a los usuarios los detalles básicos de su cuenta. No se preocupe en absoluto si le resulta difícil seguir esto; Ahora proporcionaré un código paso a paso para todo (y no hay mucho) y, al final, un enlace a un área de juegos donde puede jugar con la aplicación que funciona y mejorar su comprensión.

Primero abordemos el componente sobre la información del cliente. Creemos un archivo llamado CustomerInfo.js que contiene el siguiente código:

import React from "react";

export const CustomerInfo = () => {
  console.log("CustomerInfo was rendered! :O");
  return (
    <React.Fragment>
      <p>Name: Sam Punia</p>
      <p>Email: [email protected]</p>
      <p>Preferred method: Online</p>
    </React.Fragment>
  );
};

Nada lujoso, ¿verdad?

Solo un texto informativo (que podría haberse pasado a través de accesorios) que no se espera que cambie a medida que el usuario interactúa con la aplicación (para los puristas, sí, seguro que puede cambiar, pero el punto es que, en comparación con el resto de la aplicación, es prácticamente estática). Pero note el console.log() declaración. Esta será nuestra pista para saber que el componente fue renderizado (recuerde, "renderizado" significa que su información fue recopilada y calculada / comparada, y no que fue pintada en el DOM real).

Entonces, durante nuestras pruebas, si no vemos tal mensaje en la consola del navegador, nuestro componente no fue renderizado en absoluto; si lo vemos aparecer 10 veces, significa que el componente fue renderizado 10 veces; y así.

Y ahora veamos cómo nuestro componente principal usa este componente de información del cliente:

import React, { useState } from "react";
import "./styles.css";
import { CustomerInfo } from "./CustomerInfo";

export default function App() {
  const [totalLikes, setTotalLikes] = useState(0);
  return (
    <div className="App">
      <div className="LikesCounter">
        <p>You have liked us {totalLikes} times so far.</p>
        <button onClick={() => setTotalLikes(totalLikes + 1)}>
          Click here to like again!
        </button>
      </div>
      <div className="CustomerInfo">
        <CustomerInfo />
      </div>
    </div>
  );
}

Entonces, vemos que el App El componente tiene un estado interno gestionado a través del useState() gancho. Este estado sigue contando cuántas veces le ha gustado el servicio / sitio al usuario, y se establece inicialmente en cero. Nada desafiante en lo que respecta a las aplicaciones React, ¿verdad? En el lado de la interfaz de usuario, las cosas se ven así:

El botón parece demasiado tentador para no romperlo, ¡al menos para mí! Pero antes de hacer eso, abriré la consola de desarrollo de mi navegador y la borraré. Después de eso, voy a presionar el botón varias veces y esto es lo que veo:

Presioné el botón 19 veces y, como esperaba, el recuento total de Me gusta es de 19. La combinación de colores dificultaba mucho la lectura, así que agregué un cuadro rojo para resaltar lo principal: <CustomerInfo /> ¡El componente se renderizó 20 veces!

Por qué 20?

Una vez, cuando todo se renderizó inicialmente, y luego, 19 veces cuando se presionó el botón. El botón cambia totalLikes, que es una parte del estado dentro del <App /> componente y, como resultado, el componente principal se vuelve a renderizar. Y como hemos aprendido en las secciones anteriores de esta publicación, todos los componentes que contiene también se vuelven a renderizar. Esto no es deseado porque el <CustomerInfo /> El componente no cambió en el proceso y sin embargo contribuyó al proceso de renderizado.

¿Cómo podemos prevenir eso?

Exactamente como dice el título de esta sección, usando el memo() función para crear una copia "preservada" o en caché del <CustomerInfo /> componente. Con un componente memorizado, React mira sus accesorios y los compara con los accesorios anteriores, y si no hay cambios, React no extrae una nueva salida de "renderización" de este componente.

Agreguemos esta línea de código a nuestro CustomerInfo.js archivo:

export const MemoizedCustomerInfo = React.memo(CustomerInfo);

¡Sí, eso es todo lo que tenemos que hacer! Ahora es el momento de usar esto en nuestro componente principal y ver si algo cambia:

import React, { useState } from "react";
import "./styles.css";
import { MemoizedCustomerInfo } from "./CustomerInfo";

export default function App() {
  const [totalLikes, setTotalLikes] = useState(0);
  return (
    <div className="App">
      <div className="LikesCounter">
        <p>You have liked us {totalLikes} times so far.</p>
        <button onClick={() => setTotalLikes(totalLikes + 1)}>
          Click here to like again!
        </button>
      </div>
      <div className="CustomerInfo">
        <MemoizedCustomerInfo />
      </div>
    </div>
  );
}

Sí, solo cambiaron dos líneas, pero de todos modos quería mostrar el componente completo. Nada cambió en cuanto a la interfaz de usuario, así que si pruebo la nueva versión y aprieto el botón Me gusta varias veces, obtengo esto:

Entonces, ¿cuántos mensajes de consola tenemos?

¡Solo uno! Esto significa que, aparte del renderizado inicial, el componente no se tocó en absoluto. ¡Imagine las ganancias de rendimiento en una aplicación de gran escala! De acuerdo, de acuerdo, el enlace al área de juegos de código que prometí es aquí. Para replicar el ejemplo anterior, deberá importar y usar CustomerInfo en lugar de MemoizedCustomerInfo del CustomerInfo.js.

Dicho esto, las memo() no es arena mágica que puedes esparcir por todas partes y esperar resultados mágicos. Uso excesivo de memo() también puede introducir errores complicados en su aplicación y, a veces, simplemente hacer que algunas actualizaciones esperadas fallen. Aquí también se aplica el consejo general sobre la optimización "prematura". Primero, construya su aplicación como dice su intuición; luego, haga un perfil intensivo para ver qué partes son lentas y si parece que los componentes memorizados son la solución correcta, solo entonces introduzca esto.

Diseño de componentes "inteligente"

Puse “inteligente” entre comillas porque: 1) La inteligencia es altamente subjetiva y situacional; 2) Las acciones supuestamente inteligentes a menudo tienen consecuencias desagradables. Entonces, mi consejo para esta sección es: no confíe demasiado en lo que está haciendo.

Con eso fuera del camino, una posibilidad de mejorar el rendimiento del renderizado es diseñar y colocar los componentes de manera un poco diferente. Por ejemplo, un componente hijo se puede refactorizar y mover a algún lugar superior en la jerarquía para escapar de las re-renderizaciones. Ninguna regla dice, "el componente ChatPhotoView siempre debe estar dentro del componente Chat". En casos especiales (y estos son casos en los que tenemos evidencia respaldada por datos de que el rendimiento se está viendo afectado), doblar / romper las reglas puede ser una gran idea.

Conclusión

Se puede hacer mucho más para optimizar Reaccionar aplicaciones en general, pero como este artículo trata sobre la representación, he restringido el alcance de la discusión. Independientemente, espero que ahora tenga una mejor idea de lo que está sucediendo en React bajo el capó, qué es realmente el renderizado y cómo puede afectar el rendimiento de la aplicación.

A continuación, entendamos que es React Hooks?