Kodokon kodokon.com

Bajo el capó: reconciliación, batching, renderizado concurrente

Descubre cómo Fiber reconcilia el árbol, agrupa las actualizaciones y hace que el renderizado sea interrumpible.

11 min · 3 preguntas

Abrir esta lección en Kodokon

Desde React 16, el motor interno se llama Fiber: cada elemento de tu árbol está representado por un nodo fiber, una unidad de trabajo. React mantiene en realidad dos árboles (una técnica de doble búfer): el árbol current, que refleja lo que hay en pantalla, y el árbol workInProgress, construido durante el renderizado. El trabajo se realiza en dos fases distintas: la fase de renderizado (llamar a tus componentes, calcular las diferencias), que es interrumpible y reproducible, y luego la fase de commit (aplicar las mutaciones al DOM, ejecutar los efectos), que siempre es síncrona e ininterrumpible. La reconciliación se apoya en dos heurísticas que reducen la comparación a un coste lineal: si el tipo de un elemento cambia en una posición dada, React destruye todo el subárbol y lo reconstruye, estado incluido; para las listas, la prop key permite a React emparejar los elementos entre dos renderizados.

JSX
function List({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <input defaultValue={item.label} />
        </li>
      ))}
    </ul>
  );
}
// With key={index}, deleting the first item
// would shift the content of the remaining inputs.
Una key estable preserva el estado y el DOM de cada fila.

El segundo mecanismo fundamental: el batching. Cuando se disparan varias actualizaciones de estado dentro del mismo contexto, React las agrupa en un único renderizado. Antes de React 18, esta agrupación solo funcionaba dentro de los manejadores de eventos de React; desde createRoot, es automática en todas partes: temporizadores, promesas, listeners nativos. Por eso también leer el estado justo después de un setState devuelve el valor antiguo: la actualización está programada, no aplicada.

JSX
function handleTick() {
  setCount((c) => c + 1);
  setFlag((f) => !f);
  // React 18: a single render, even here.
}

setTimeout(handleTick, 100);
// Before React 18, this timer would trigger
// two successive renders.
El batching automático ahora cubre todos los contextos.

El renderizado concurrente lleva la lógica más lejos: la fase de renderizado puede ser interrumpida, reanudada, o incluso descartada y reiniciada si llega una actualización más urgente. React ordena las actualizaciones por prioridad (los lanes): una pulsación de tecla es urgente, un filtro de lista marcado con startTransition no lo es. La consecuencia directa: tus funciones de componente deben ser puras, porque React puede llamarlas varias veces sin llegar nunca a hacer commit del resultado. Otro caso límite: un store externo (Redux, Zustand, un singleton casero) leído durante un renderizado interrumpible puede cambiar a mitad del renderizado, produciendo una interfaz inconsistente conocida como tearing. La API useSyncExternalStore resuelve este problema forzando un renderizado síncrono cuando el store cambia.

JAVASCRIPT
import { useSyncExternalStore } from 'react';

function useWindowWidth() {
  return useSyncExternalStore(
    (notify) => {
      window.addEventListener('resize', notify);
      return () =>
        window.removeEventListener('resize', notify);
    },
    () => window.innerWidth
  );
}
Leer un store externo sin riesgo de tearing.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. Durante la reconciliación, ¿qué hace React si un elemento <div> se convierte en un <span> en la misma posición?
    • Actualiza la etiqueta conservando los hijos
    • Destruye todo el subárbol, estado incluido, y lo reconstruye
    • Compara recursivamente los hijos antes de decidir
    • Lanza un error de reconciliación
  2. Con React 18 y createRoot, ¿cuántos renderizados disparan dos llamadas a setState dentro de un setTimeout?
    • Dos, porque el batching solo se aplica a los eventos de React
    • Solo uno, gracias al batching automático
    • Cero, las actualizaciones fuera de los eventos se ignoran
  3. ¿Qué fase del trabajo de React es interrumpible en modo concurrente?
    • Solo la fase de commit
    • Solo la fase de renderizado
    • Ambas fases
    • Ninguna, React permanece totalmente síncrono