wandres.dev
UNTRACK, BATCH, ON · controlar el tracking

batch: una sola propagación

batch agrupa varias escrituras para que memos y efectos se recalculen una sola vez al final, no una por cada setter. Cómo Solid ya auto-agrupa las escrituras que nacen dentro de una computación, por qué las escrituras asíncronas escapan a ese auto-batching, y en qué casos batch sigue siendo la diferencia entre una pasada y muchas.

⏱ 14 min

Cuando cambias tres signals seguidos, ¿tus efectos corren una vez o tres? La respuesta gobierna el rendimiento y la consistencia de toda aplicación reactiva. Solid resuelve buena parte del problema solo, agrupando de forma automática las escrituras que nacen dentro de su propio ciclo. Pero hay una frontera —la asincronía— donde ese auto-agrupado se desactiva y cada setter vuelve a propagarse por su cuenta. batch es la herramienta para trazar esa frontera a mano: abre un único ciclo alrededor de varias escrituras y fusiona su propagación en una sola pasada.

🎯 Al terminar esta lección sabrás
  • Entender qué hace batch: agrupar escrituras en una única propagación diferida.
  • Comprender el auto-batching: por qué escribir dentro de una computación ya se agrupa solo.
  • Reconocer la frontera asíncrona donde el auto-batching deja de aplicar.
  • Saber cuándo batch sigue aportando y cuándo es redundante.

Una escritura, un ciclo; muchas escrituras, un solo ciclo

batch recibe una función, ejecuta todas las escrituras que contenga y difiere la propagación hasta que la función termina. Los efectos y los memos que dependían de esos signals se recalculan una sola vez al final, con el estado ya completo, en lugar de una vez por cada setter. Devuelve lo que la función retorne: batch<T>(fn: () => T): T.

import { createSignal, createMemo, batch } from "solid-js";

const [nombre, setNombre] = createSignal("Ada");
const [apellido, setApellido] = createSignal("Lovelace");

const completo = createMemo(() => {
  console.log("recalculo");
  return `${nombre()} ${apellido()}`;
});

batch(() => {
  setNombre("Grace");
  setApellido("Hopper");
}); // "recalculo" se imprime UNA vez, no dos

Sin el batch, cada setter abriría y cerraría su propia pasada, y todo lo que dependa de ambos signals correría dos veces, observando de paso un estado intermedio absurdo —“Grace Lovelace”— que nunca fue real. Con batch, esa incoherencia transitoria no llega a existir: la interfaz salta del estado viejo al nuevo sin escalas.

Dentro del batch, los setters siguen aplicándose de inmediato para las lecturas directas de signals; lo que se aplaza hasta el cierre es la re-ejecución de los efectos y las re-renderizaciones. Es decir: no congelas los valores, congelas la notificación.

El auto-batching: el ciclo reactivo ya es un lote

Aquí está la clave que casi nadie explica bien. Solid ya agrupa solo en un caso importantísimo, y entender por qué te dice exactamente cuándo batch sobra.

Cuando un setter corre y no hay ningún ciclo de actualización en curso, Solid abre uno, aplica el cambio y, al cerrarlo, vacía la cola de efectos. Pero si el setter corre mientras un ciclo ya está abierto, la escritura se une al ciclo en marcha en vez de abrir el suyo. ¿Y cuándo hay un ciclo abierto? Precisamente cuando escribes desde dentro de una computación: el cuerpo de un createEffect, de un createMemo, de un createComputed. La propia ejecución del sistema reactivo es un lote.

createEffect(() => {
  // ya estamos dentro de un ciclo: estas dos escrituras se auto-agrupan
  setNombre("Grace");
  setApellido("Hopper");
  // los efectos dependientes corren una vez al cerrar este ciclo
});

Por eso, dentro de efectos, escribir varios signals seguidos no multiplica las pasadas: el auto-batching lo cubre gratis. batch ahí sería redundante.

flowchart TD
S[varios setters seguidos] --> Q{hay un ciclo abierto}
Q -->|si dentro de efecto o batch| U[se unen a un solo cierre]
Q -->|no escritura asincrona suelta| M[cada uno abre su propio cierre]
U --> R[los efectos corren una vez]
M --> T[los efectos corren varias veces]
style U fill:#a6e3a1,color:#11111b
style R fill:#a6e3a1,color:#11111b
style M fill:#f38ba8,color:#11111b
style T fill:#f38ba8,color:#11111b

Leer dentro del lote

Un matiz que sorprende a quien piensa batch como una transacción congelada: dentro del lote, si lees un signal que acabas de escribir, obtienes el valor nuevo, no el viejo. Los setters aplican su valor de inmediato; lo único que se aplaza es la notificación a los efectos. Se difiere reaccionar, no escribir.

const [x, setX] = createSignal(1);
batch(() => {
  setX(2);
  console.log(x());   // imprime 2: la escritura ya está aplicada
  setX(3);
  console.log(x());   // imprime 3
}); // los efectos que dependen de x corren una vez, y ven 3

Los memos son un caso intermedio que conviene tener claro: como son perezosos, leer un memo dentro del lote después de cambiar sus fuentes lo recalcula en el acto y te da el valor fresco. Lo que no ocurre hasta cerrar el batch es la re-ejecución de los efectos y el repintado del DOM. Dicho de otro modo, el lote te garantiza consistencia de lectura —siempre ves lo último— y solo aplaza el trabajo observable hacia afuera.

Y como batch devuelve lo que su función retorne, puedes agrupar y computar en una sola expresión, sin una variable temporal fuera del lote:

const resumen = batch(() => {
  setUsuario(u);
  setPermisos(p);
  return `${u.nombre}: ${p.length} permisos`; // este valor sale del batch
});

Los batch anidados no apilan capas: un batch dentro de otro se funde con el externo, porque ya hay un ciclo abierto y la escritura se une a él —la misma regla del auto-batching—. No existe, pues, el peligro de “sobre-agrupar” por composición: envolver de más es, como mucho, redundante, nunca incorrecto. Esto hace que batch sea seguro de aplicar en funciones reutilizables sin saber si el llamador ya está dentro de otro lote.

La frontera asíncrona: donde batch vuelve a importar

El auto-batching cubre las escrituras que nacen dentro del ciclo reactivo. Fuera de él, cada setter abre y cierra su propia pasada. Y hay un lugar donde siempre estás fuera del ciclo: los callbacks asíncronos. Un setTimeout, un .then de una promesa, la respuesta de un fetch, un mensaje de WebSocket, un requestAnimationFrame —todos corren después de que el ciclo original se cerró—.

async function cargar() {
  const datos = await fetch("/api").then((r) => r.json());
  // fuera de todo ciclo reactivo: sin batch, cada setter propaga por separado
  batch(() => {
    setUsuario(datos.usuario);
    setPermisos(datos.permisos);
    setCargando(false);
  });
}

Sin el batch, esos tres setters disparan tres propagaciones, y un efecto que dependa de los tres correría tres veces, viendo estados a medio hidratar. Con batch, la interfaz pasa de “cargando” a “listo” en un solo salto coherente. Esta es la regla operativa: en código asíncrono que escribe varios signals relacionados, agrupa.

ℹ️
El store también se beneficia

Cuando actualizas varias rutas de un store con setStore en sucesión —o combinas setStore con setters de signals sueltos— batch fusiona todas esas mutaciones en una sola propagación. Es especialmente útil al hidratar un store completo desde una respuesta de red: envuelve el bloque de setStore y evitarás que cada campo dispare su propia cascada de efectos y su propio repintado.

📝
Solid nunca muestra un DOM roto, ni sin batch

Un matiz importante para no malinterpretar batch: aunque omitas el agrupado, Solid jamás renderiza un DOM inconsistente a medio actualizar. Las lecturas son perezosas y el grafo se resuelve en orden topológico, así que el resultado final siempre es correcto. Lo que batch evita no es la incorrección, sino el trabajo redundante: recomputaciones y repintados intermedios que nadie llega a ver pero que igual cuestan CPU. batch es una optimización de eficiencia y de coherencia observable por los efectos, no un parche de corrección.

batch no cambia el resultado: cambia cuántas veces el sistema cree que el mundo cambió

La tentación al aprender batch es pensarlo como un candado que protege una transacción, por analogía con las bases de datos. La analogía engaña. Solid no necesita batch para ser consistente: su núcleo ya garantiza que ningún efecto observe un estado a medias en la propagación final y que el DOM nunca quede roto entre dos setters. Lo que batch gobierna es otra cosa —cuántas veces el sistema cree que el mundo cambió—, y esa es una pregunta de eficiencia, no de correción. Cada propagación es una pasada por el grafo: memos que se invalidan, efectos que se encolan, el DOM que se reconcilia. Si tres escrituras conceptualmente forman un solo cambio —“el usuario terminó de cargar”— pero las ejecutas como tres eventos, le pides al sistema que recorra el grafo tres veces para llegar al mismo sitio, y de paso expones a tus efectos a dos estados intermedios que no significan nada. batch declara la verdad semántica: esto es un solo cambio, aunque toque varios signals. Por eso el auto-batching de Solid es tan elegante: reconoce que la ejecución de una computación ya es, por naturaleza, una unidad de cambio, y agrupa sus escrituras sin que lo pidas. La única grieta es la asincronía, porque un callback que llega tres segundos después no puede saber a qué ciclo pertenecía —no pertenece a ninguno—, y ahí eres tú quien debe declarar la unidad con batch. Interiorizado esto, la regla se vuelve trivial de aplicar: no envuelvas por superstición, ni escribas setters sueltos en un .then por descuido. Pregúntate si las escrituras son un solo hecho o varios; si son uno, dilo con batch; si el ciclo ya está abierto, Solid ya lo dijo por ti.

⚔️ Cuenta las pasadas
  1. Crea un memo con un console.log que dependa de dos signals y cámbialos seguidos fuera de todo efecto; cuenta cuántas veces se imprime.
  2. Envuelve esas dos escrituras en batch y confirma que el log baja a una sola línea.
  3. Repite las dos escrituras dentro de un createEffect sin batch y verifica que el auto-batching ya las agrupa: una sola pasada.
  4. Simula una carga con setTimeout que escribe tres signals; observa las propagaciones sin batch y luego con batch alrededor.
  5. Explica con tus palabras por qué batch es redundante dentro de un efecto pero imprescindible dentro de un .then.