wandres.dev
LOTES Y PLANIFICACIÓN · Batching y el momento de ejecutar

El lote como transacción

Agrupar varias escrituras para que produzcan una sola propagación no es solo una optimización: es lo que hace que un estado compuesto de varias señales tenga estados intermedios inobservables.

⏱ 16 min

Cuando un cambio conceptual requiere escribir tres señales, entre la primera y la tercera el sistema pasa por estados que no corresponden a ninguna situación real de la aplicación. Un lote convierte esas tres escrituras en una transacción con un único punto de confirmación, y con ello no solo ahorra trabajo: elimina una categoría entera de estados imposibles que el modelo de señales atómicas introduce por construcción.

🎯 Al terminar esta lección sabrás
  • Implementar un lote reentrante correcto.
  • Distinguir el ahorro de trabajo de la garantía de atomicidad.
  • Reconocer los estados imposibles que produce escribir sin lote.
  • Aplicar el lote automático que ofrecen los motores reales.

El problema sin lote

Considera un cambio de página en una lista paginada. Conceptualmente es una operación; mecánicamente son tres escrituras.

function irAPagina(n) {
  pagina.set(n);
  desplazamiento.set(n * tamano());
  seleccion.set([]);
}

Sin lote, cada escritura propaga por separado. Entre la primera y la segunda, el sistema tiene la página nueva con el desplazamiento viejo, y cualquier efecto que dependa de ambos se ejecuta con esa combinación. Entre la segunda y la tercera, la selección todavía apunta a filas de la página anterior.

Un efecto que cargue datos vería tres estados distintos y lanzaría tres peticiones, dos de ellas con parámetros incoherentes. No es un glitch en el sentido del nivel 5 —cada estado intermedio es internamente consistente, porque cada uno refleja el valor real de las señales en ese instante— sino algo distinto y más difícil de razonar: la aplicación pasa por estados que su propia lógica de dominio considera imposibles.

La implementación

Un lote es una cola de efectos y un marcador de que hay una transacción abierta.

let Lote = null;

function lote(fn) {
  if (Lote) return fn();                    // ya hay uno abierto, nos sumamos
  const cola = Lote = new Set();
  try {
    return fn();
  } finally {
    for (const efecto of cola) {
      cola.delete(efecto);
      if (efecto.estado !== LIMPIO) actualizar(efecto);
    }
    Lote = null;
  }
}

function planificar(efecto) {
  if (Lote) Lote.add(efecto);
  else lote(() => Lote.add(efecto));        // una escritura suelta abre su propio lote
}

Tres detalles merecen atención.

La reentrancia. Si ya hay un lote abierto, la llamada anidada simplemente ejecuta el cuerpo sin abrir otro. Esto hace que anidar lotes sea seguro y que una función que agrupa internamente se pueda llamar desde dentro de otro agrupamiento sin efectos raros.

El vaciado dentro del finally. Si el cuerpo lanza una excepción, los efectos pendientes se ejecutan igualmente. Es discutible —hay argumentos para descartarlos— pero la alternativa deja el sistema con nodos marcados que nadie va a resolver.

El uso de un conjunto. Deduplica: si tres escrituras marcan el mismo efecto, solo se ejecuta una vez. Y como se itera con for...of sobre un Set, los efectos añadidos durante el vaciado también se recorren, lo que da soporte natural a las cascadas.

⚠️
Lote no es lo mismo que asincrono

Un lote agrupa escrituras dentro de un tramo síncrono y ejecuta los efectos al final de ese tramo, todavía de forma síncrona. No difiere nada al bucle de eventos. La diferencia con el modelo asíncrono —donde los efectos esperan a un microtask— es el tema de la lección 3 de este nivel, y son decisiones independientes: se pueden combinar las cuatro formas.

Lo que se gana

Trabajo. Con k escrituras que afectan al mismo efecto, se pasa de k ejecuciones a una. En una operación que toca veinte señales leídas por cien efectos, la diferencia es de dos mil ejecuciones a cien.

Atomicidad observable. Ningún efecto ve un estado intermedio de la transacción. Es la garantía que hace razonable escribir varias señales para un solo cambio conceptual.

Un único punto de coherencia. Todos los efectos de un lote se ejecutan viendo el mismo estado global. Dos efectos distintos no pueden ver combinaciones distintas de las mismas señales.

flowchart TB
subgraph sin lote
A1[escribir pagina] --> P1[propagar y ejecutar efectos]
P1 --> A2[escribir desplazamiento]
A2 --> P2[propagar y ejecutar efectos]
P2 --> A3[escribir seleccion]
A3 --> P3[propagar y ejecutar efectos]
end
subgraph con lote
B1[escribir las tres] --> M[marcar sin ejecutar]
M --> P[una sola propagacion al cerrar]
end
style A1 fill:#89b4fa,color:#11111b
style A2 fill:#89b4fa,color:#11111b
style A3 fill:#89b4fa,color:#11111b
style P1 fill:#f38ba8,color:#11111b
style P2 fill:#f38ba8,color:#11111b
style P3 fill:#f38ba8,color:#11111b
style B1 fill:#89b4fa,color:#11111b
style M fill:#f9e2af,color:#11111b
style P fill:#a6e3a1,color:#11111b

Lo que se pierde y el lote automático

Lo que se pierde

Merece la pena ser honesto con las contrapartidas, porque las hay.

Leer dentro del lote devuelve el valor nuevo. Las escrituras se aplican inmediatamente; lo que se aplaza es la ejecución de los efectos. Si dentro del lote lees una señal que acabas de escribir, ves el valor nuevo. Eso es lo esperable y a la vez rompe la analogía con una transacción de base de datos, donde el aislamiento sería total.

El punto de confirmación se aleja del punto de escritura. Si el efecto falla, la traza apunta al cierre del lote y no a la escritura que lo provocó. Depurar cuesta más.

Las cascadas se enredan. Un efecto que escribe dentro del vaciado añade más efectos a la misma cola, y esa cola se sigue vaciando. Razonar sobre el orden en una cascada larga es difícil, y es el tema de la lección 5.

El lote automático

Los motores reales agrupan solos en los sitios donde el agrupamiento es claramente correcto, para que no haya que escribirlo a mano constantemente.

Solid agrupa automáticamente dentro de los efectos y de las transiciones, y expone batch para el resto. Vue agrupa por defecto todo lo que ocurre en un mismo tick, porque su planificador es asíncrono por diseño. Angular agrupa mediante su planificador de detección de cambios: varias escrituras en el mismo turno producen una sola pasada. Preact Signals expone batch explícito. Svelte agrupa las actualizaciones dentro de un mismo tick.

La consecuencia práctica es que en muchos motores el lote explícito solo hace falta cuando escribes varias señales fuera de un contexto ya agrupado, típicamente en un manejador de eventos o en un callback asíncrono.

El lote hace que un estado repartido se comporte como uno solo

La razón profunda por la que este mecanismo es imprescindible en motores de señales atómicas —y no en motores con un almacén único— es una consecuencia directa de la decisión de repartir el estado. Cuando el estado vive en un objeto único que se reemplaza de una vez, la atomicidad es gratis: hay una sola escritura y no existe el intervalo entre dos escrituras. Al repartir el estado en cincuenta señales independientes —que es lo que compra la granularidad fina— se introduce la posibilidad de estados intermedios que ningún estado único tendría. El lote es el mecanismo que devuelve la atomicidad perdida. Visto así, hay una simetría que ordena todo el diseño: la granularidad fina te da actualizaciones baratas y te quita la atomicidad; el lote te devuelve la atomicidad a cambio de tener que marcar las fronteras de la transacción. Y de ahí sale una regla práctica que vale para cualquier motor de esta familia: cada vez que un cambio conceptual toque más de una señal, envuélvelo en un lote y dale nombre a esa función. La función con nombre es la unidad de cambio de tu dominio, y el lote es lo que hace que se comporte como una unidad de verdad. Los sistemas donde esto se descuida acaban con efectos que se disparan de más y con bugs que solo aparecen si las escrituras van en cierto orden, que son de los más difíciles de reproducir que existen.

⚔️ Mide el ahorro y la atomicidad
  1. Escribe una operación que modifique tres señales y un efecto que dependa de las tres.
  2. Cuenta las ejecuciones del efecto con y sin lote.
  3. Registra los valores que el efecto observa en cada ejecución sin lote e identifica los estados que tu dominio considera imposibles.
  4. Anida un lote dentro de otro y comprueba que los efectos se ejecutan una sola vez, al cerrar el externo.