Batching: agrupar la propagación
El orden topológico cura los glitches dentro de una propagación, pero varios cambios seguidos disparan varias propagaciones, cada una recorriendo el grafo y exhibiendo estados intermedios que nadie pidió ver. El batching agrupa múltiples escrituras en una sola propagación: coalesce el trabajo y oculta los estados intermedios porque los efectos solo corren cuando el lote entero se ha asentado. Esta lección distingue el batching explícito del automático por microtarea y lo sitúa como la consistencia en el tiempo, complementaria al orden topológico.
El orden topológico resuelve un problema espacial: dado UN cambio, en qué orden recorrer el grafo para no ver inconsistencias. Pero las aplicaciones reales no cambian un dato a la vez: un manejador de evento escribe tres señales seguidas, una respuesta de red actualiza cinco campos. Si cada escritura dispara su propia propagación, el grafo se recorre tantas veces como escrituras, y entre una y otra queda expuesto en estados que son consistentes pero no deseados: reflejan una actualización a medio aplicar. El batching es la respuesta a este problema temporal: agrupar las escrituras y propagar una sola vez, al final. Es la consistencia en el tiempo, hermana del orden topológico que da la consistencia en el espacio.
- Ver por qué varias escrituras seguidas causan propagaciones repetidas y estados intermedios no deseados.
- Distinguir un estado intermedio inconsistente —un
glitch— de uno consistente pero prematuro. - Usar
batchpara coalescer escrituras y correr los efectos una sola vez con valores finales. - Entender el batching automático por microtarea y cómo lo implementan los sistemas de 2026.
El problema: muchas escrituras, muchas propagaciones
Imagina un nombre compuesto de dos señales y un derivado que las une.
const nombre = signal("Ada")
const apellido = signal("King")
const completo = computed(() => `${nombre()} ${apellido()}`)
// actualizamos a Ada Lovelace en dos pasos
setNombre("Ada") // completo se recalcula a "Ada King": intermedio no deseado
setApellido("Lovelace") // completo se recalcula a "Ada Lovelace": final
Aquí no hay glitch: “Ada King” fue un estado perfectamente consistente del grafo —existió mientras solo el apellido estaba pendiente—. El problema es otro: es un estado que nunca quisimos exponer. Si un efecto guarda el nombre en disco o lo envía a un servidor, guardará “Ada King”, un valor que jamás fue la intención del programa. Y el grafo se recorrió dos veces cuando una habría bastado.
Conviene separar dos cosas que el nivel trata juntas bajo la etiqueta de estado intermedio. Un glitch es intermedio E inconsistente: no corresponde a ninguna configuración real de las fuentes, y lo cura el orden topológico. El intermedio del batching es consistente pero prematuro: corresponde a una configuración real —solo cambió el nombre—, pero es una que el programa consideraba un paso interno de una actualización mayor. El orden topológico no puede ocultarlo porque no es un error de orden; hace falta agrupar en el tiempo. Ambos comparten el síntoma: un observador ve algo que no debía.
batch: coalescer y ocultar
El batch difiere la propagación hasta que un bloque de escrituras termina, y entonces propaga una sola vez. Las escrituras dentro del lote solo marcan sus nodos; nada se recalcula ni ningún efecto corre hasta cerrar el bloque.
batch(() => {
setNombre("Ada")
setApellido("Lovelace")
})
// una sola propagacion al cerrar el lote:
// completo se recalcula UNA vez y vale "Ada Lovelace"
// ningun efecto vio jamas "Ada King"
Por dentro, un batch no es más que un contador de profundidad y un conjunto de nodos pendientes. Mientras haya un lote abierto, escribir solo apila; al cerrar el último, se drena todo de una vez, en orden topológico, y recién entonces corren los efectos.
let profundidad = 0
const pendientes = new Set()
function batch(fn) {
profundidad++
try {
fn() // las escrituras de fn solo apilan en pendientes
} finally {
profundidad--
if (profundidad === 0) drenar() // solo el batch mas externo propaga
}
}
function marcarSucio(nodo) {
pendientes.add(nodo)
if (profundidad === 0) drenar() // sin batch abierto: propaga al instante
}
function drenar() {
const cono = ordenarPorAltura(pendientes) // orden topologico
pendientes.clear()
for (const n of cono) n.recalcular()
correrEfectos() // los efectos, una vez, al final
}
El contador profundidad resuelve el anidamiento con elegancia: si un batch llama a una función que a su vez abre otro batch, solo el más externo dispara el drenar. Los lotes anidados se funden en una única propagación, así que componer funciones que cada una agrupa sus escrituras no multiplica las pasadas —igual que una transacción anidada en una base de datos: solo el commit exterior es visible—. El batching rinde entonces en las dos direcciones que nos importan: en trabajo, varias escrituras que comparten dependientes recorren el grafo una vez; en consistencia, el estado prematuro “Ada King” nunca se materializa para un observador. Un lote es, en la práctica, una transición atómica de un estado consistente y deseado a otro.
Batching automático por microtarea
Escribir batch a mano en cada sitio sería tedioso y fácil de olvidar. Los sistemas modernos añaden batching implícito programando el vaciado —el flush— como una microtarea al final del tick síncrono actual. Cada escritura marca su nodo como sucio y, si aún no hay un flush programado, encola uno; todas las escrituras síncronas del mismo tick se funden en esa única pasada.
contador.set(1)
contador.set(2)
contador.set(3)
// sin batch explicito: tres marcas de sucio, un solo flush en la microtarea
// los efectos corren una vez y observan 3
sequenceDiagram participant U as codigo de usuario participant G as grafo participant E as efectos U->>G: setNombre Ada marca sucio U->>G: setApellido Lovelace marca sucio U->>E: fin del tick programa un solo flush E->>G: recalcula completo una vez con valores finales E->>E: los efectos corren una vez con Ada Lovelace
Cada ecosistema tiene su variante de esta idea, y en 2026 la convergencia es casi total. React agrupa de forma automática todas las actualizaciones de un tick desde la versión 18, incluso dentro de promesas y temporizadores, y ofrece startTransition para diferir actualizaciones de baja prioridad. Solid y Preact Signals exponen un batch explícito además de coalescer efectos. Vue programa su cola de efectos y la vacía en el nextTick. Angular corre los efectos de señales en una fase planificada, glitch-free y agrupada. Svelte 5 vacía sus runes en una microtarea. La propuesta de Signals de TC39 deja el flush en manos de un Watcher que el consumidor programa: el batching es una política sobre cuándo drenar los efectos pendientes.
El batching automático solo alcanza a las escrituras del mismo tick síncrono. Una escritura tras un await o dentro de un setTimeout cae en otra tarea del bucle de eventos y abre su propia propagación. Ese fue justo el agujero que React 18 tapó al extender el agrupamiento a promesas y temporizadores: antes, dos actualizaciones tras un await disparaban dos renders. Si necesitas agrupar escrituras que cruzan esa frontera asíncrona, tienes que envolverlas tú mismo en un batch explícito.
No confundas las dos garantías. El orden topológico decide en qué SECUENCIA se recalcula el grafo durante una propagación; el batching decide CUÁNTAS propagaciones hay y cuándo ocurren. Puedes tener orden topológico sin batching —cada escritura propaga bien, pero hay muchas— y batching sin orden topológico —una sola pasada, pero con glitches internos—. Un sistema serio quiere ambos: agrupar las escrituras en una única propagación con el batching y recorrer esa propagación en orden de dependencias con la topología. Juntos garantizan que el mundo exterior solo observe estados consistentes y deseados.
Sabores de flush
Todos los sistemas agrupan, pero no todos vacían el lote en el mismo momento, y esa elección de cuándo drenar tiene consecuencias visibles.
Síncrono
El flush ocurre al cerrar el batch, en la misma pila. Simple y predecible, pero cada lote explícito paga su propagación de inmediato.
Microtarea
El flush se pospone al final del tick con una microtarea. Coalesce todas las escrituras síncronas sin que las envuelvas en un batch, a cambio de que los efectos corran un instante después.
Por frame
Para efectos que tocan el DOM, algunos motores alinean el flush con el frame de render mediante requestAnimationFrame, y así evitan trabajo visual redundante entre dos pinturas.
Por prioridad
Los planificadores concurrentes difieren los efectos de baja prioridad —lo que React expone como transiciones— para que una actualización urgente no espere a otra costosa.
La regla de diseño es que los efectos que producen salida observable —pintar, guardar, pedir— deben correr una sola vez por lote y con valores finales, sea cual sea el sabor de flush. El cuándo se elige por latencia y por el tipo de efecto; el cuántas veces siempre debe ser uno. Cuando batching y orden topológico colaboran, esa promesa se cumple por construcción, y el resto son detalles de sintonía fina sobre en qué instante exacto libera el sistema su trabajo acumulado.
La forma más honda de entender el batching es como transaccionalidad. Una base de datos no deja que un observador vea el estado a medio camino de una transacción: mientras se ejecuta, los cambios son invisibles fuera; al confirmarse, aparecen todos de golpe; el exterior solo percibe estados confirmados, nunca los pasos intermedios. El batching lleva esa misma disciplina al grafo reactivo. Entre el instante en que se abre el lote y aquel en que se cierra, el grafo puede pasar por configuraciones internas que serían prematuras o incluso inconsistentes, pero nadie las observa porque los efectos —la única forma que tiene el grafo de hablarle al mundo— se retienen hasta el asentamiento. Al cerrar, el sistema recorre una sola vez el cono de nodos afectados, en orden topológico, y solo entonces libera los efectos, ya con valores finales. Así, el orden topológico garantiza la consistencia dentro de una propagación —consistencia en el espacio del grafo— y el batching garantiza que las propagaciones se agrupen en transiciones atómicas —consistencia en el tiempo—. La reactividad madura no es más que esto: llevar al estado en memoria las mismas garantías de aislamiento y atomicidad que dábamos por sentadas en las bases de datos, para que ningún observador, ni en el espacio ni en el tiempo, pueda leer jamás una verdad a medio hacer.
- Crea
nombre,apellidoycompletocon un efecto que registre cada valor; actualiza ambos sinbatchy cuenta las ejecuciones y los valores intermedios. - Envuelve las dos escrituras en
batchy comprueba que el efecto corre una sola vez y nunca ve el estado prematuro. - Anida dos
batchy verifica que solo el cierre del más externo dispara la propagación. - Haz tres
setsíncronos sobre una señal sinbatchexplícito y averigua si tu librería agrupa por microtarea; cuenta losflush. - Mueve una de las escrituras tras un
awaity observa cómo el batching implícito ya no la agrupa; luego arréglalo con unbatchexplícito.