El problema común: consistencia incremental
Todos los motores resuelven la misma tarea formal — recomputar el subconjunto mínimo de un grafo de derivaciones tras una modificación — y se diferencian solo en qué aproximación de mínimo aceptan y qué pagan por ella.
Debajo de la diversidad de sintaxis hay una única tarea formal, y tiene nombre en la literatura: computación incremental. Dada una función que produce una salida a partir de una entrada, y dado un cambio pequeño en esa entrada, producir la nueva salida haciendo un trabajo proporcional al cambio y no al tamaño total del problema. Todos los motores del track son aproximaciones prácticas a ese ideal, y todos fallan al ideal de maneras concretas que conviene conocer.
- Enunciar la tarea formal que resuelven todos los motores.
- Distinguir el mínimo teórico de las aproximaciones que se implementan de verdad.
- Identificar las tres fuentes de trabajo excedente que ningún motor evita del todo.
- Justificar por qué el grano de la derivación determina el mínimo alcanzable.
La tarea formal
Sea f una función pura de un estado s a una salida o. Ya tienes o = f(s) calculado. Ahora s cambia a s' por un delta pequeño. La tarea es producir o' = f(s') con un coste proporcional al tamaño del delta y al de la diferencia entre o y o', no al tamaño de s ni al de o.
Enunciado así se ve inmediatamente por qué es difícil. f es código arbitrario: bucles, condicionales, llamadas. No hay manera general de saber qué partes de f dependían de la parte de s que cambió sin ejecutar f otra vez. La computación incremental general es, en el caso peor, tan cara como recalcular.
Los motores reactivos escapan de esa imposibilidad con un truco: no tratan f como una caja negra. Obligan al programador a descomponer f en muchas funciones pequeñas conectadas explícitamente, y aplican la incrementalidad al grafo de esas piezas, no al interior de cada pieza. Dentro de un nodo se recalcula todo; entre nodos se recalcula solo lo alcanzable.
flowchart LR S[estado s] --> F[funcion f monolitica] F --> O[salida o] S2[estado s] --> N1[nodo a] S2 --> N2[nodo b] N1 --> N3[nodo c] N2 --> N3 N3 --> O2[salida o] style S fill:#89b4fa,color:#11111b style F fill:#f38ba8,color:#11111b style O fill:#a6e3a1,color:#11111b style S2 fill:#89b4fa,color:#11111b style N1 fill:#cba6f7,color:#11111b style N2 fill:#cba6f7,color:#11111b style N3 fill:#cba6f7,color:#11111b style O2 fill:#a6e3a1,color:#11111b
Arriba, la función monolítica: cualquier cambio obliga a reejecutarla entera. Abajo, la misma función descompuesta: un cambio que solo afecta a b deja a intacto. El grano de la descomposición fija el mínimo que el motor puede alcanzar. Si el programador escribe un único nodo gigante, ningún motor lo salvará; si lo descompone en cincuenta nodos, un buen motor recalculará tres.
Las tres fuentes de trabajo excedente
Ningún motor real alcanza el mínimo teórico. El exceso viene siempre de tres sitios, y saber cuál domina en tu caso es el primer paso de cualquier diagnóstico serio.
Sobreaproximación del alcance. El motor recalcula un nodo porque una entrada suya cambió, aunque el resultado del nodo sea idéntico. Es lo que ocurre cuando esValido depende de texto y texto pasa de "hola" a "holas": esValido sigue siendo true, pero el motor no lo sabe hasta recalcularlo. La memoización con comparación de igualdad —el nivel 6— existe justo para cortar la propagación en ese punto, y no puede evitar el recálculo del propio nodo, solo el de sus descendientes.
Contabilidad del grafo. Mantener aristas cuesta. Cada lectura dentro de una computación implica dos inserciones en estructuras de datos, y cada reejecución implica desatar todas las aristas viejas antes de tejer las nuevas. En grafos con muchas aristas y computaciones baratas, la contabilidad puede costar más que el cómputo que ahorra. Este es el argumento serio contra la reactividad de grano muy fino, y es real.
Granularidad fija. El motor no puede recalcular media derivación. Si un nodo produce una lista de mil elementos y uno cambia, el nodo se recalcula entero. La única salida es que el programador cambie la descomposición: un nodo por elemento en vez de un nodo para la lista. Ninguna de esas decisiones la puede tomar el motor por ti.
Cada técnica que reduce una de las tres fuentes aumenta otra. Los memos reducen la sobreaproximación y aumentan la contabilidad. Un grano más fino reduce la granularidad fija y multiplica las aristas. Un grano más grueso reduce las aristas y aumenta la sobreaproximación. No hay configuración que gane en las tres, y por eso la pregunta correcta nunca es cuánto trabajo excedente hay, sino cuál de los tres domina en este caso concreto.
Consistencia, y no solo eficiencia
La eficiencia es la mitad del problema. La otra mitad es que el resultado sea correcto, y correcto tiene un significado técnico que no es obvio.
Considera a = 1, b = a + 1, c = a + b. Con a = 1, tenemos b = 2 y c = 3. Ahora escribimos a = 10. Si el motor recalcula c antes que b, obtendrá c = 10 + 2 = 12, un valor que no corresponde a ningún estado consistente del sistema: no es el c de antes ni el de después. Si además algo observa c en ese instante —un efecto que escribe en el DOM, un log, una petición— ese valor imposible se hace visible al mundo.
A ese valor intermedio incorrecto se le llama glitch, y a la propiedad de no producirlos nunca se le llama consistencia glitch-free. Es una garantía que un motor puede ofrecer o no, y la diferencia es observable. Le dedicamos el nivel 5 completo.
Aquí basta con retener la conclusión estructural: la consistencia obliga a un orden, y el orden obliga a conocer la topología. Un motor que solo sepa quiénes son los vecinos inmediatos de un nodo no puede garantizar glitch-freedom, porque no sabe si c tiene que esperar a b. Esa es, en última instancia, la razón última por la que existe el grafo explícito.
El error de expectativa más común con los motores de grano fino es esperar que compensen una mala descomposición. No lo hacen y no pueden. Un motor de reactividad es un optimizador sobre el grafo que le das; si le das un grafo con un solo nodo enorme, hará exactamente el mismo trabajo que un ciclo de re-render completo, con la contabilidad de aristas encima. Es exactamente el mismo fenómeno que con una base de datos: el planificador de consultas es brillante, pero si el esquema está mal normalizado no hay índice que te salve. La consecuencia práctica es incómoda y conviene decirla claro: cambiar de framework rara vez arregla un problema de rendimiento reactivo, porque el problema casi nunca está en el motor sino en la forma del grafo que tu código construye. Lo que sí cambia entre frameworks es cuánto te cuesta —en ergonomía y en líneas— describir un grafo bien granulado. Esa es la diferencia real, y es de diseño de API, no de algoritmo.
Lo que esto implica para el resto del track
Si todos los motores resuelven la misma tarea, entonces estudiarlos uno a uno es redundante. Lo productivo es estudiar las decisiones que hay que tomar al construir uno, porque son las mismas en todos, y luego ver qué eligió cada uno.
Las decisiones son estas, y son exactamente el índice de este track: cómo representar el grafo, cómo descubrir las aristas, cómo propagar el cambio, cómo garantizar el orden, cuándo memoizar, quién limpia las suscripciones muertas, cuándo ejecutar los efectos, y si el estado se modela átomo a átomo o como objeto profundo. Ocho decisiones. Cada motor real es una tupla de ocho respuestas.
- Escribe una cadena de tres derivaciones sobre una fuente, con un contador de ejecuciones en cada una.
- Escribe la fuente veinte veces con valores que no cambien el resultado de la primera derivación.
- Cuenta cuántas veces se ejecutó cada nivel de la cadena y clasifica el exceso en una de las tres fuentes.
- Cambia la descomposición para reducir el exceso y comprueba qué otra fuente aumentó a cambio.