Bailout: cuándo React se saltea tu componente
Mi componente se renderiza y no cambió nada. ¿Se puede evitar?
Al terminar vas a poder explicar
- Enumerar las condiciones exactas que producen un bailout
- Explicar por qué memo solo suele no alcanzar
- Decidir con criterio cuándo memoizar y cuándo no tocar nada
Por defecto, cuando un componente se re-renderiza, todos sus descendientes se re-renderizan. No porque sus props hayan cambiado, sino porque el padre produjo elementos nuevos y React no tiene motivo para suponer que son equivalentes.
Pero React sí tiene un camino de escape: el bailout. Cuando lo toma, ni siquiera ejecuta la función del componente. Se saltea la rama entera.
Las condiciones exactas
En beginWork, antes de llamar a tu función, React chequea si puede zafar. Necesita las tres:
- Existe un fiber current. No es un montaje.
- Las props son las mismas. Por identidad —
oldProps === newProps— o, si el componente está envuelto enmemo, por comparación superficial. - Este fiber no tiene trabajo agendado. Sus
lanesestán en cero: nadie llamó a susetState.
Ahí está el detalle que explica todo. En condiciones normales, la condición 2 nunca se cumple: cada render del padre crea elementos nuevos, con objetos de props nuevos. { nombre: 'Ana' } === { nombre: 'Ana' } es false.
Por eso existe memo: cambia la comparación de identidad por una comparación superficial clave por clave.
Un hijo memoizado que React se saltea por completo
AGENDADOpaso 1 / 110Sin hooks en este paso.
Árbol de fibers
- <HostRoot>
Salida
Nada en el host todavía. React no commiteó ningún cambio.
createRoot().render()
Montaje inicial. No hay árbol previo, así que todo se va a crear desde cero.
VER EL CÓDIGO QUE SE ESTÁ TRAZANDO
1const Hijo = memo(function Hijo({ etiqueta }) {2 return <span>{etiqueta}</span>;3});4 5function Padre() {6 const [n, setN] = useState(0);7 const alClick = useCallback(() => {}, []);8 9 return (10 <div>11 <button onClick={() => setN(v => v + 1)}>padre: {n}</button>12 <Hijo etiqueta="no cambio nunca" alClick={alClick} />13 </div>14 );15}Con el foco acá: ← → un paso · espacio reproduce · Inicio/Fin van a los extremos.
Buscá el paso bailout. El componente Hijo tiene renderCount en 1 después de dos renders del padre: su función se ejecutó una sola vez. En el árbol de fibers vas a ver la marca ⊘ bailout.
Por qué memo solo casi nunca alcanza
La comparación superficial usa Object.is en cada prop. Alcanza con que una prop sea una referencia nueva para que el bailout no ocurra. Y esto pasa todo el tiempo:
1function Padre() {2 const [n, setN] = useState(0);3 4 return (5 <Hijo6 alClick={() => hacerAlgo()} // ✗ función nueva en cada render7 opciones={{ modo: "claro" }} // ✗ objeto nuevo en cada render8 items={datos.filter(Boolean)} // ✗ array nuevo en cada render9 titulo="Panel" // ✓ string primitivo: siempre igual10 />11 );12}Tres de las cuatro props rompen el memo. memo sin estabilizar las props es overhead puro: pagás la comparación y no ganás nada.
El bailout se propaga hacia abajo
Cuando un fiber hace bailout, React clona sus hijos sin ejecutar ninguna función de componente. Esos hijos clonados llegan a beginWork con las mismas referencias de props que tenían, así que también hacen bailout. Y así hasta abajo.
Un solo memo bien puesto arriba de un subárbol puede saltear cientos de componentes. Cien memo mal puestos no saltean ninguno.
Antes de memoizar, movete
Muchas veces el problema se resuelve sin memoizar nada, sólo cambiando dónde vive el estado. Si un estado que cambia seguido está más arriba de lo que necesita, bajalo:
1// Antes: el estado del input está en el padre, así que2// TODO el subárbol se re-renderiza en cada tecla.3function Panel() {4 const [texto, setTexto] = useState("");5 return (6 <>7 <input value={texto} onChange={e => setTexto(e.target.value)} />8 <ListaEnorme />9 </>10 );11}12 13// Después: el estado vive donde se usa. ListaEnorme ni se entera.14function Buscador() {15 const [texto, setTexto] = useState("");16 return <input value={texto} onChange={e => setTexto(e.target.value)} />;17}18 19function Panel() {20 return (21 <>22 <Buscador />23 <ListaEnorme />24 </>25 );26}Este movimiento no agrega API, no agrega comparaciones y no se puede romper por accidente. Siempre es preferible a un memo.
Antes de seguir · predecí
Envolviste Hijo en memo y sigue renderizándose cada vez. Sus props son title (un string fijo) y items (un array que sale de props.datos.map(...) en el padre). ¿Qué pasa?
Lo que te llevás
El bailout necesita props idénticas y cero trabajo agendado. memo cambia la comparación de identidad por una superficial, pero sólo funciona si el padre manda referencias estables. Antes de memoizar, fijate si podés bajar el estado: es gratis y no se rompe.
Antes de marcarla, comprobá
- ¿Podés explicar enumerar las condiciones exactas que producen un bailout?
- ¿Podés explicar explicar por qué memo solo suele no alcanzar?
- ¿Podés explicar decidir con criterio cuándo memoizar y cuándo no tocar nada?