Skip to content
This page has been auto-translated and may contain errors.View in English

Cómo React renderiza

"Render" es una de las palabras más usadas en React y una de las menos examinadas. Entender qué sucede realmente entre una actualización de estado y los píxeles cambiando en la pantalla aclara mucho: por qué los componentes se ejecutan cuándo lo hacen, por qué la mayoría de los re-renders cuesta prácticamente nada, y dónde debería enfocarse el trabajo real de optimización. Este capítulo construye ese modelo mental. Los dos capítulos siguientes, Memoización y Code splitting, lo ponen en práctica.

Un render es una llamada a función

Cuando React "renderiza" un componente, llama a tu función de componente:

jsx
function Counter() {
  const [count, setCount] = useState(0)
  console.log('Counter renderizado') // se ejecuta cada vez que React llama esta función

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

Ese es el truco completo. La función se ejecuta de principio a fin, cualquier cálculo dentro de ella se ejecuta nuevamente, y devuelve JSX describiendo cómo debería verse la UI ahora. Nada de eso involucra el DOM del navegador. Un render produce una descripción; no toca la página.

Esa distinción importa porque estas dos cosas tienen costos totalmente diferentes. Llamar una función JavaScript y construir una descripción es rápido. Cambiar el DOM real, lo que dispara el trabajo de layout y paint en el navegador, es la parte costosa. Todo el diseño de React se inclina hacia hacer la cosa barata libremente y la cosa costosa lo menos posible.

Los tres pasos

Un único "re-render" atraviesa tres pasos. El disparador es generalmente una actualización de estado, aunque el mount inicial, un componente padre renderizando arriba del componente, y un nuevo valor de context ejecutan la misma maquinaria.

  1. Render. React llama la función del componente cuyo estado cambió, y luego, recursivamente, cada componente que devuelve. El resultado es un DOM virtual fresco: una descripción ligera en memoria de esa parte de la UI.
  2. Reconciliación. React ejecuta un algoritmo de diffing comparando el nuevo DOM virtual contra el anterior, averiguando exactamente qué cambió entre ellos.
  3. Commit. Las diferencias, y solo las diferencias, se aplican al DOM real. Esta es la parte que el usuario realmente puede ver.

El curso ofrece una analogía que vale la pena mantener en mente. Un arquitecto actualizando un edificio primero redibuja el plano (render), luego lista qué cambios el nuevo plano realmente requiere (reconciliación), y solo entonces la cuadrilla de construcción toca el edificio (commit). Redibujar un plano es rápido. La construcción es donde van el tiempo y el dinero, así que la cuadrilla hace el menor trabajo que la lista permite.

El renderizado y la reconciliación se mantienen rápidos, y el commit se limita a lo que el diff encontró, por eso React se siente rápido por defecto. Cuando algo sí se vuelve lento, el Profiler te dice cuál paso culpar en lugar de dejarte adivinando.

Cada clic en el botón Counter ejecuta los tres: el log se dispara, React difea el resultado, y el commit actualiza el nodo de texto del botón y nada más a su alrededor.

El renderizado es recursivo, y eso generalmente está bien

Cuando un componente renderiza, React renderiza todo lo que devuelve, luego todo lo que esos componentes devuelven, hasta el fondo de cada rama antes de pasar a la siguiente. Un cambio de estado en un componente padre por lo tanto re-renderiza todos sus descendientes por defecto, sin importar si reciben el estado cambiado como props.

Eso suena derrochador hasta que separas los pasos nuevamente. Esos renders hijo son llamadas a función produciendo descripciones. Durante la reconciliación, React no encuentra atributos cambiados en ninguno de ellos, así que el commit no tiene trabajo de DOM que hacer para ese subárbol. El trabajo de DOM visible al usuario se mantiene limitado a lo que realmente cambió. Re-renderizar un subárbol que no hace commit es el caso normal y saludable, y para la inmensa mayoría de los componentes es demasiado barato para medir.

Deja de estar bien en dos situaciones: cuando un componente hace trabajo costoso durante su render, o cuando un subárbol es tan grande que incluso los renders baratos se suman. Esos son exactamente los casos que Memoización aborda. Resiste la tentación de alcanzarla antes de tener evidencia, lo que nos lleva a medir.

Mide antes de optimizar

React ya es rápido, y la reconciliación ya mantiene las actualizaciones del DOM mínimas. El consejo del curso es directo: sé capaz de medir un problema de rendimiento antes de aplicar una técnica para arreglarlo, y pesa cada optimización contra la legibilidad del código que complica. La astucia que nadie puede mantener también es un costo.

Dos herramientas cubren la mayoría de necesidades de medición:

  • La pestaña Performance del navegador. Graba una sesión, realiza la interacción lenta, detente, e inspecciona qué se ejecutó y cuánto tiempo tomó. Su throttling de CPU (prueba un desaceleramiento de 4x o 6x) y throttling de red te permiten que tu máquina de desarrollo rápida simule los dispositivos más lentos y conexiones que tus usuarios realmente tienen. Los problemas invisibles en tu laptop aparecen inmediatamente.
  • El Profiler de React DevTools. Una extensión de navegador del equipo de React, viviendo en su propia pestaña Profiler. El mismo flujo de graba-actúa-detente, pero la salida es de forma React: qué componentes renderizaron, por qué renderizaron, cuánto tiempo tomó cada uno, y cuánto tiempo tomó el commit. Para diagnosticar apps de React específicamente, generalmente es la herramienta más directa.

StrictMode: un pasamanos para desarrollo

StrictMode es un componente que envuelves alrededor de tu app (o cualquier subárbol) para activar chequeos solo en desarrollo. Es el wrapper que Setup pone alrededor de <App /> en main.jsx.

En desarrollo, StrictMode deliberadamente invoca dos veces tus funciones de componente, junto con las otras funciones que React espera que sean puras: inicializadores de useState y useReducer, los updaters que pasas a un setter, reducers, y cálculos de useMemo. Ejecuta cada efecto una vez extra, configurándolo, limpiándolo, y configurándolo de nuevo, y reinyecta callbacks de ref. También advierte sobre APIs deprecadas. En una build de producción no hace nada, así que nunca ralentiza o re-renderiza dos veces la app que tus usuarios ven.

Ejecutar dos veces suena como sabotaje hasta que ves qué atrapa. Una función de componente se supone que es pura: mismos inputs, mismo output, sin efectos secundarios durante el render. Si renderizar dos veces produce un resultado diferente que renderizar una vez, el componente estaba escondiendo un bug.

El curso demuestra esto con un componente que llama push en un array definido fuera de sí mismo durante el render; la página se ve bien hasta que cualquier cambio de estado re-ejecuta la función y hace push de un duplicado. Bajo StrictMode el duplicado aparece en el primer load, y la solución (copiar el array antes de modificarlo, o mover el trabajo fuera del render) se sigue de ello.

La pureza cubre tus updaters

El mismo requisito de pureza cubre el updater en setCount(c => c + 1), ya que esa función también se ejecuta dos veces.

Las pruebas de re-ejecutar efecto prueban el mismo contrato desde el otro lado. Effects dueño de la regla de cleanup y muestra cómo se ve una faltante bajo StrictMode, cuando un setInterval sin limpiar te deja contando de dos en dos y filtrando un timer por mount.

Ver más bugs en desarrollo se siente al revés al principio, pero ese es el punto entero. Cada bug que StrictMode expone ya existía y de otro modo habría esperado a producción para presentarse.

Nota de versión

El comportamiento de efecto llegó en React 18: al mount, StrictMode ejecuta la setup de cada efecto, luego su cleanup, luego setup de nuevo. Artículos más viejos describen StrictMode como solo re-renderizando componentes. El curso escribe el wrapper como React.StrictMode; con named imports es <StrictMode>.

El paso de diffing es más barato que lo que "comparar dos árboles" sugiere porque React se rehúsa a hacer una comparación de árbol completo, que sería O(n³) en el caso general. Las heurísticas: elementos de tipos diferentes desmontan el subárbol viejo completamente y reconstruyen, elementos del mismo tipo se mantienen y solo sus atributos cambiados se parchean, y la reconciliación de listas es guiada por keys, por eso las keys deben ser identidades estables.

React alcanza Object.is en las comparaciones que controlan el trabajo: salir de una actualización cuando estableces state al mismo valor, chequear arrays de dependencia, y la comparación shallow de props de memo. Dos objetos profundamente iguales con identidades diferentes fallan los tres. Este es el hilo que corre a través del próximo capítulo: un padre pasando {} o una arrow function inline crea una identidad fresca cada render, que es inofensiva por defecto pero anula cada optimización basada en identidad el momento en que añades una.

Los propios docs de React agrupan los primeros dos pasos en una única fase render: el diffing sucede conforme React camina el árbol elemento por elemento, así que nada espera a que una descripción completa sea construida primero. Un bailout puede por lo tanto detener parcialmente abajo en una rama. Separar la reconciliación es una conveniencia de enseñanza para describir qué hace el diff, y el modelo de dos fases render/commit es el que el Profiler y el código fuente de React usan.

Esa fase render es también, por diseño, permitida ser tirada a la basura. React puede comenzar a renderizar, descartar el trabajo, y renderizar de nuevo antes de hacer commit, y las características concurrentes se apoyan en esto para mantener el main thread responsivo.

Esa es la razón más profunda por la que los renders deben ser puros: React solo garantiza que el trabajo commiteado sucede exactamente una vez. Cualquier cosa que un componente hace durante la fase render (mutaciones, suscripciones, logging en el que confías) puede suceder cero, una, o varias veces. El double-invoke de StrictMode es una simulación barata de esa realidad en desarrollo, y el código que se quiebra bajo ella es código que el renderizado concurrente puede quebrar de verdad.

Cuando el Profiler sí muestra un problema real, empareja la herramienta con el paso. Barras de render largas en un componente haciendo computación pesada apuntan a useMemo; un subárbol ancho re-renderizando sin nada para hacer commit apunta a memo; ambos viven en Memoización. Una carga inicial lenta, donde el problema es JavaScript llegando antes de que cualquier renderizado pueda comenzar, es un problema de bundle más que un problema de render, y Code splitting es la palanca.

A veces la solución es estructural: mover state abajo al componente que lo usa, o pasar subárboles costosos como children para que el re-render del padre stateful vea los mismos objetos elemento y salga de esa rama, remueve renders sin ninguna memoización en absoluto.

JunoLos renders son baratos, los commits son precisos Un render significa que React llama tu función de componente de nuevo para preguntarle cómo debería verse la pantalla. Compara la nueva respuesta con la vieja y actualiza solo las partes de la página que realmente cambiaron. Cuando un padre renderiza, todos sus hijos renderizam también, y eso es normal y casi siempre rápido.

Envuelve tu app en StrictMode mientras construyes: ejecuta cosas dos veces a propósito en desarrollo para ayudarte a detectar bugs temprano, y se apaga a sí mismo en la app terminada.

JunoLos renders son baratos, los commits son precisos Tres pasos: render llama tus funciones de componente y construye un nuevo DOM virtual, reconciliación lo difea contra el viejo, commit aplica solo las diferencias al DOM real. Los renders padre se desmoronan a los hijos por defecto, y como la mayoría de esos renders no hacen commit, cuestan prácticamente nada.

Antes de optimizar cualquier cosa, graba la interacción lenta en el Profiler de React DevTools y throttla tu CPU para ver qué ven los usuarios.

Mantén StrictMode encendido: los renders invocados dos veces exponen componentes impuros, y re-ejecutar efectos expone cleanup faltante como un setInterval sin limpiar.

JunoLos renders son baratos, los commits son precisos La reconciliación es heurística: cambios de tipo reconstruyen subárboles, elementos del mismo tipo se parchean en lugar, keys impulsan diffing de listas, y las comparaciones que controlan el trabajo (state bailout, arrays de dependencia, memo) se ejecutan en Object.is, así que identidades de objeto y función frescos se leen como cambios.

El trabajo de la fase render es descartable por contrato, por eso la pureza importa y por qué StrictMode simula la ejecución doble que el renderizado concurrente puede producir.

Perfila primero, luego empareja la solución con la fase: memoriza renders costosos, divide bundles para cargas lentas, o reestructura con children para que el trabajo nunca suceda en absoluto.

Siguiente: Memoización, donde la igualdad referencial decide qué puede saltarse React.