wandres.dev
PUSH, PULL Y PUSH-PULL · Los tres modelos de propagación

Los tres modelos, medidos

El coste de escritura, lectura y memoria de push puro, pull puro e híbrido sobre los mismos grafos, con el banco de pruebas que lo mide y la interpretación de dónde gana cada uno.

⏱ 17 min

Tres modelos, tres perfiles de coste. Esta lección los pone en la misma tabla sobre los mismos grafos, con el banco de pruebas que puedes ejecutar, y saca la conclusión que importa: el híbrido no gana en todas las casillas, gana en la mezcla de operaciones que produce una interfaz de usuario. Cambia la mezcla y el ganador cambia.

🎯 Al terminar esta lección sabrás
  • Comparar los tres modelos por coste de escritura, lectura y memoria.
  • Construir un banco de pruebas que mida cuerpos ejecutados y no tiempos.
  • Interpretar los resultados según la mezcla de operaciones.
  • Identificar la mezcla característica de una interfaz de usuario.

La tabla

Con A el número de aristas del subgrafo alcanzable desde la fuente escrita, P la profundidad del camino desde una hoja hasta las raíces, y C el número de nodos cuyo valor de verdad cambia.

operación push puro pull puro híbrido
escritura O de A, con cuerpos ejecutados O de 1 O de A, solo marcas
lectura de un nodo limpio O de 1 O de P verificando O de 1
lectura de un nodo marcado O de 1 O de P mas recálculos O de P mas C recálculos
cuerpos ejecutados por escritura todos los alcanzables solo los leídos solo los leídos y afectados
memoria por nodo valor y observadores valor, fuentes y versiones valor, ambas listas y estado
glitches no no
efectos posibles no, sin sondeo

Las dos filas que deciden son las dos últimas de comportamiento. Push puro produce glitches, que es un fallo de corrección. Pull puro no puede ejecutar efectos, que es una carencia funcional. El híbrido no tiene ninguno de los dos defectos, y ese es el motivo real de su dominio; el rendimiento es la guinda.

El banco de pruebas

Medir tiempos en JavaScript es ruidoso. Lo que de verdad distingue a los modelos es cuántos cuerpos de derivación se ejecutan, y eso se cuenta de forma exacta y reproducible.

function bancoDePruebas(crear, escenario) {
  let cuerpos = 0, marcas = 0;
  const contar = (fn) => (...args) => { cuerpos++; return fn(...args); };
  const motor = crear({ alEjecutar: () => cuerpos++, alMarcar: () => marcas++ });
  escenario(motor, contar);
  return { cuerpos, marcas };
}

// Escenario 1: cadena profunda con un memo que corta
function cadenaConCorte(motor) {
  const a = motor.senal(0);
  const par = motor.memo(() => a() % 2 === 0);
  let ultimo = par;
  for (let i = 0; i < 9; i++) {
    const previo = ultimo;
    ultimo = motor.memo(() => previo() ? 'si' : 'no');
  }
  motor.efecto(() => ultimo());
  for (let i = 0; i < 100; i += 2) a.set(i);   // el booleano nunca cambia
}

Cien escrituras sobre una cadena de diez derivaciones donde el primer memo devuelve siempre el mismo booleano. Los resultados esperados son muy distintos entre modelos.

Push puro: mil cuerpos. Cada escritura recorre y ejecuta los diez nodos.

Pull puro: cien cuerpos si nadie lee, más los que provoque cada lectura. Como el efecto necesita un sondeo, en la práctica depende de la frecuencia del bucle.

Híbrido: cien cuerpos. Se ejecuta solo el primer memo en cada escritura; como devuelve el mismo booleano, la cadena entera se queda en QUIZA y pasa a limpio sin ejecutarse.

Un factor diez en un caso que no es artificial: una cadena de derivaciones sobre un valor que cambia mucho y produce un resultado estable es exactamente lo que ocurre con validaciones, banderas de estado y clases de CSS.

El escenario donde el híbrido no gana

Es importante construir también el caso contrario, porque existe.

// Escenario 2: todos los nodos cambian siempre y todo se lee siempre
function todoCambia(motor) {
  const a = motor.senal(0);
  const memos = [];
  let previo = a;
  for (let i = 0; i < 10; i++) {
    const p = previo;
    previo = motor.memo(() => p() + 1);
    memos.push(previo);
  }
  motor.efecto(() => previo());
  for (let i = 0; i < 100; i++) a.set(i);
}

Aquí cada memo produce un valor distinto en cada escritura y el efecto lo lee siempre. Los tres modelos ejecutan mil cuerpos: el trabajo es irreducible. Y el híbrido paga de más: la fase de marcado recorre el subgrafo entero antes de que la fase de pull lo recorra otra vez. Dos recorridos donde push puro hace uno.

La conclusión es honesta: cuando todo cambia y todo se lee, el híbrido es el más lento de los tres por una constante. Que gane en la práctica se debe a que esa situación es rarísima en una interfaz real.

flowchart TB
E1[escenario cadena con corte] --> P1[push puro mil cuerpos]
E1 --> H1[hibrido cien cuerpos]
E2[escenario todo cambia] --> P2[push puro mil cuerpos un recorrido]
E2 --> H2[hibrido mil cuerpos dos recorridos]
style E1 fill:#89b4fa,color:#11111b
style E2 fill:#89b4fa,color:#11111b
style P1 fill:#f38ba8,color:#11111b
style H1 fill:#a6e3a1,color:#11111b
style P2 fill:#a6e3a1,color:#11111b
style H2 fill:#f9e2af,color:#11111b

La mezcla de una interfaz y lo que queda

La mezcla característica de una interfaz

Lo que decide es el perfil de operaciones, y una interfaz tiene uno muy marcado.

Muchas más escrituras que cambios reales. Un campo de texto emite una escritura por tecla; la mayoría no cambia el resultado de las validaciones que cuelgan de él. Un puntero emite cientos de eventos por segundo; casi ninguno cambia la celda sobre la que está.

Muchas más derivaciones que salidas visibles. En cualquier pantalla hay más estado derivado que píxeles que dependan de él, porque hay paneles cerrados, listas virtualizadas y rutas inactivas.

Cadenas de derivación cortas pero anchas. Profundidad tres o cuatro, anchura de cientos. Es la forma que vimos en el nivel 2.

Con esa mezcla, el híbrido gana cómodamente porque las dos ventajas que aporta —cortar la cadena cuando un valor no cambia, y no calcular lo que nadie lee— son exactamente las que la mezcla premia.

⚠️
Cuidado con extrapolar de las comparativas

Las comparativas públicas de frameworks miden casi siempre el segundo escenario: cambiar mil filas, reemplazar todo el conjunto de datos, ordenar una tabla entera. Son operaciones donde todo cambia y todo se lee, es decir, justo donde el híbrido pierde su ventaja y donde lo que se está midiendo es en realidad la eficiencia de la capa de renderizado. Extrapolar de ahí a este motor reactivo es más rápido es un error de razonamiento frecuente.

El hibrido gana por lo que no hace, y eso no se mide con un cronometro

La conclusión del nivel es un poco incómoda para quien busca una respuesta simple. El modelo híbrido no es más rápido ejecutando: es más rápido no ejecutando, y esa es una virtud que ninguna medición de tiempo por operación captura bien. Si mides el coste de una escritura aislada, el híbrido parece peor que pull puro, porque pull puro solo incrementa un contador. Si mides el coste de una lectura aislada, parece peor que push puro, porque push puro ya lo tenía todo calculado. Solo cuando mides una secuencia realista de escrituras y lecturas mezcladas aparece la ventaja, y aparece como una ausencia: cuerpos que no corrieron. De aquí sale la regla metodológica que de verdad sirve cuando optimices un sistema reactivo: instrumenta cuerpos ejecutados, no milisegundos. Los milisegundos te dirán que todo va bien porque cada nodo cuesta microsegundos; el conteo de ejecuciones te dirá que estás corriendo cuatro mil cuerpos donde deberías correr cuarenta, y ese número sí señala la causa. Es exactamente el mismo motivo por el que en una base de datos se cuentan filas examinadas y no tiempo de consulta: la métrica de trabajo evitado es la que tiene poder de diagnóstico, y la de tiempo solo confirma el síntoma.

Lo que queda pendiente

El modelo híbrido resuelve el trabajo inútil y ordena el cálculo. Lo que todavía no hemos demostrado es que elimine los glitches en todos los casos, y no en el ejemplo concreto que hemos visto. Para eso hace falta mirar el rombo con cuidado, entender qué garantiza exactamente el marcado en dos fases, y compararlo con la alternativa clásica de ordenar el grafo topológicamente. Es el nivel 5 completo.

⚔️ Construye la tabla con tus propios numeros
  1. Implementa los tres modelos con la misma interfaz y un contador de cuerpos ejecutados.
  2. Ejecuta los dos escenarios de esta lección y rellena la tabla.
  3. Diseña un tercer escenario con la mezcla característica de una interfaz: muchas escrituras, pocos cambios, la mitad de las hojas sin leer.
  4. Comprueba si el orden de los tres modelos cambia y explica por qué a partir de la mezcla.