Las cuatro familias de motores
Reconciliación por diffing, grafo de señales, streams de eventos y transformación en compilación. Cuatro respuestas mecánicamente distintas al mismo problema, con la estructura de datos que caracteriza a cada una.
Cuando un motor de reactividad se enfrenta a la pregunta de qué recalcular tras un cambio, hay cuatro respuestas históricamente distintas, y cada una define una familia entera de herramientas. No se distinguen por su sintaxis ni por su ecosistema, sino por la estructura de datos que mantienen en memoria y por el momento en que deciden. Aprender a clasificar un motor por esa estructura te ahorra años de discusiones estériles sobre cuál es mejor.
- Clasificar un motor por la estructura de datos que mantiene entre cambios.
- Distinguir reconciliación por diffing de grafo de dependencias explícito.
- Situar los streams de eventos como una familia con garantías distintas.
- Reconocer qué parte del trabajo mueve a compilación cada familia.
Familia 1: reconciliación por comparación
La primera familia no mantiene ningún grafo de dependencias. Su apuesta es la contraria: no rastrear nada y, cuando algo cambia, volver a producir una descripción completa del resultado deseado y compararla con la anterior.
La estructura que persiste entre cambios no es un grafo, es un árbol de salida: la representación de lo que se produjo la última vez. El algoritmo central no es una propagación, es un diff. React es el ejemplar canónico, con su árbol de fibras; también lo son la mayoría de los motores de plantillas de servidor con hidratación.
// Esquema del ciclo, sin dependencias rastreadas
function ciclo(estadoNuevo) {
const arbolNuevo = render(estadoNuevo); // reconstruye la descripcion entera
const parches = diff(arbolAnterior, arbolNuevo);
aplicar(parches); // toca el mundo real
arbolAnterior = arbolNuevo;
}
La ventaja es enorme y se subestima con frecuencia: el modelo mental del programador es una función pura del estado a la vista, sin ninguna suscripción que gestionar y sin ninguna fuga posible, porque no hay nada a lo que suscribirse. El coste es igual de claro: el trabajo por cambio es proporcional al tamaño de la salida que se vuelve a describir, no al tamaño del cambio.
Familia 2: grafo de dependencias explícito
La segunda familia hace exactamente lo contrario: mantiene en memoria un grafo dirigido de nodos reactivos y aristas de dependencia, y cuando algo cambia recorre solo la parte alcanzable desde el nodo escrito.
La estructura que persiste es el grafo en sí. El algoritmo central es un recorrido con marcado. Solid, Vue, Angular, Preact Signals, MobX y Knockout pertenecen todos a esta familia, y las diferencias entre ellos —que veremos en el nivel 10— son detalles de implementación sobre la misma idea.
// Esquema del ciclo, con dependencias rastreadas
function escribir(fuente, valor) {
fuente.valor = valor;
for (const observador of fuente.observadores) marcarSucio(observador);
vaciarCola(); // solo lo alcanzable se recalcula
}
El trabajo por cambio es proporcional al número de nodos realmente afectados, con independencia del tamaño total de la aplicación. A cambio, hay que pagar por mantener el grafo: memoria por nodo, memoria por arista, y el coste de tejer y desatar aristas en cada ejecución.
Familia 3: streams de eventos
La tercera familia modela el cambio como una secuencia de eventos en el tiempo, no como un valor que se actualiza. RxJS, Bacon y los Observables clásicos viven aquí; también, en parte, los AsyncIterable del propio lenguaje.
La diferencia con la familia 2 es profunda y a menudo se pasa por alto. Un nodo del grafo de señales tiene un valor actual que se puede leer en cualquier momento; un stream, en cambio, tiene una historia y puede no tener valor actual en absoluto. Eso cambia las garantías: un stream puede modelar naturalmente cosas que un signal no —un canal sin memoria, un evento que ocurre pero no deja estado, la composición temporal con operadores como debounce o switchMap— y a cambio no puede garantizar consistencia glitch-free sin trabajo adicional, porque no hay una noción global de estado actual sobre la que definir la consistencia.
Angular es el caso más visible de convivencia: tiene señales en el núcleo y RxJS en el ecosistema, con puentes explícitos entre ambos mundos a través de toSignal y toObservable en @angular/core/rxjs-interop. La regla práctica que emerge de ese diseño es sensata: el estado actual se modela con señales, el flujo temporal —cancelación, reintentos, ventanas de tiempo— con streams.
La cuarta familia, y por qué esto no es una jerarquía
Familia 4: transformación en compilación
La cuarta familia mueve parte del trabajo a antes de que el programa corra. En lugar de descubrir dependencias observando ejecuciones, un compilador analiza el código fuente y emite las suscripciones, o directamente el código de actualización.
La estructura que persiste en tiempo de ejecución puede ser mínima, porque parte de la información ya está codificada en las instrucciones generadas. Svelte con sus runes y el compilador JSX de Solid son los ejemplos más claros; React Compiler es un caso híbrido interesante porque no cambia de familia —React sigue reconciliando— sino que automatiza la memoización dentro de la familia 1.
Ninguna herramienta real es puramente de esta cuarta familia. El compilador siempre deja un runtime, porque hay dependencias que solo se conocen al ejecutar. La cuarta familia no sustituye a las otras: reduce la parte de las otras que hay que pagar en tiempo de ejecución. Es una transformación, no una arquitectura independiente, y el nivel 11 se dedica entero a delimitar hasta dónde llega.
flowchart TB P[cambio en el estado] --> F1[Familia 1 reconciliacion] P --> F2[Familia 2 grafo de senales] P --> F3[Familia 3 streams] P --> F4[Familia 4 compilacion] F1 --> R1[compara arbol nuevo con viejo] F2 --> R2[recorre solo lo alcanzable] F3 --> R3[emite el evento por el canal] F4 --> R4[ejecuta el codigo ya emitido] style P fill:#f9e2af,color:#11111b style F1 fill:#89b4fa,color:#11111b style F2 fill:#cba6f7,color:#11111b style F3 fill:#94e2d5,color:#11111b style F4 fill:#fab387,color:#11111b style R1 fill:#a6e3a1,color:#11111b style R2 fill:#a6e3a1,color:#11111b style R3 fill:#a6e3a1,color:#11111b style R4 fill:#a6e3a1,color:#11111b
Cuando te encuentres con un motor nuevo, ignora por completo su documentación de portada y hazle una sola pregunta: qué guarda en memoria entre dos cambios consecutivos. Si guarda una copia de la salida anterior, es familia 1 y su coste será proporcional al tamaño de la salida. Si guarda nodos con listas de observadores, es familia 2 y su coste será proporcional a las dependencias afectadas, más una factura fija de memoria por arista. Si guarda suscriptores sin valor actual, es familia 3 y tendrás que resolver tú la consistencia. Si no guarda casi nada porque las decisiones están en el código emitido, mira qué dejó el compilador y descubrirás que casi siempre es una de las tres anteriores en miniatura. Esta pregunta única predice, sin leer una línea de la documentación, el perfil de rendimiento, el modo de fallo típico y la clase de bug que vas a depurar a las tres de la mañana. Los eslóganes de marketing cambian cada dos años; la estructura de datos no.
Por qué la clasificación no es una jerarquía
Es tentador leer esta lista como una progresión histórica hacia lo mejor, y sería un error. Las cuatro familias siguen vivas porque optimizan cosas distintas y porque el criterio de optimización depende de la aplicación.
Un editor de texto colaborativo con cientos de miles de nodos y cambios muy localizados quiere la familia 2. Una aplicación de formularios con árboles pequeños y lógica de negocio pesada apenas nota la diferencia y puede preferir la simplicidad de la familia 1. Un sistema de telemetría con eventos que llegan sin parar y necesitan agregación temporal quiere la familia 3, y forzarlo a señales sería reimplementar mal la mitad de RxJS.
La lección del nivel 1 se dedica a la primera distinción de esta lista —familia 1 frente a familia 2— con números, porque es la que más ruido genera y la que más se malinterpreta.
- Elige cinco librerías o frameworks que hayas usado, incluida al menos una que no sea de interfaz de usuario.
- Para cada una, responde solo a la pregunta clave: qué guarda en memoria entre dos cambios.
- Asígnale una familia y anota la evidencia concreta —un nombre de clase, un campo, una función del código fuente— que respalda tu clasificación.
- Encuentra al menos una que sea claramente híbrida y describe qué parte pertenece a cada familia.