wandres.dev
EL GRAFO REACTIVO · observers y sources

Comparación: MobX, Angular, Preact y qué hace único a Solid

En 2026 casi todo el frontend converge en el mismo algoritmo de signals: un oyente global, un grafo bidireccional y push-pull con coloreado de nodos. MobX, los signals de Angular y de Preact, y la propuesta de TC39 comparten el motor. Lo que separa a Solid no es el algoritmo del grafo, sino que ese grafo ES el renderizador: sin virtual DOM, sin re-render, con el componente corriendo una sola vez.

⏱ 18 min

Has visto el motor de Solid por dentro: nodos, listas bidireccionales, push-pull, coloreado de estados, un oyente global. La sorpresa, al asomarse fuera, es cuánto de esto no es exclusivo de Solid. En 2026 los sistemas de signals serios —MobX, Angular, Preact, Vue, Qwik y la propia propuesta de TC39— convergen en el mismo algoritmo de grafo. La pregunta interesante ya no es “¿cómo funciona un signal?”, que todos responden parecido, sino “¿en qué se diferencian?” y, sobre todo, “¿qué hace realmente único a Solid?”. La respuesta no está en el grafo, sino en lo que Solid conecta al otro extremo del grafo.

🎯 Al terminar esta lección sabrás
  • Reconocer el algoritmo común que comparten los sistemas de signals modernos.
  • Situar MobX, los signals de Angular y los de Preact frente a Solid.
  • Distinguir la sintaxis de lectura y la estrategia de render de cada uno.
  • Nombrar con precisión qué hace único a Solid: el grafo como renderizador.

El algoritmo común

Casi todos los sistemas modernos comparten la misma columna vertebral, la que acabas de estudiar en Solid:

📍

Un oyente global

Una variable de módulo apunta a la computación en curso. En Solid es Listener; en MobX, la trackingDerivation; en Angular y Preact, su consumidor activo. Leer consulta ese puntero.

🔗

Grafo bidireccional

Fuentes y consumidores se enlazan en ambos sentidos para propagar hacia abajo y desuscribir con eficiencia. Arrays con slots, listas enlazadas o versiones: la idea es la misma.

🎨

Push-pull coloreado

Nodos limpios, a-comprobar y sucios. La escritura marca barato hacia abajo; la lectura o el flush recomputan caro y solo lo necesario. Glitch-free por el mismo orden topológico.

💤

Derivaciones perezosas

Los valores derivados se cachean y solo se recalculan cuando una fuente cambia de verdad. La pereza a nivel de valor es universal; la pereza a nivel de lectura varía.

Que MobX pionerara este esquema en 2015 y que la propuesta de signals de TC39 lo estandarice una década después dice mucho: el algoritmo del grafo reactivo es, a estas alturas, teoría asentada. Lo describen todos igual —incluso comparten el vocabulario del graph coloring—. Por eso comparar frameworks por su motor de signals es cada vez menos útil: casi todos tienen buen motor. Las diferencias reales están en la sintaxis de lectura y en qué hacen con el grafo. La primera es superficial pero visible; la segunda es profunda y define la arquitectura. Empecemos por la superficial:

// El MISMO contador, cuatro sintaxis de lectura distintas:
count();          // Solid y Angular: llamada a funcion
count.value;      // Preact: acceso a una propiedad .value
pedido.total;     // MobX: propiedad transparente interceptada por un proxy
count.get();      // TC39 Signal.State: un metodo get explicito

Bajo esas cuatro caras late el mismo gesto: la lectura consulta al oyente global y, si lo hay, teje la arista. La sintaxis solo decide dónde se engancha el getter —en una función, en un descriptor de propiedad, en una trampa de proxy o en un método—. Nada de esto cambia el algoritmo; cambia la ergonomía y, como verás al final, la facilidad para componer sin perder reactividad.

💡
No elijas framework por su motor de signals

Una consecuencia práctica de esta convergencia: comparar frameworks por “lo bueno que es su sistema de signals” es, en 2026, casi discutir de gustos. Casi todos rondan el mismo algoritmo y un rendimiento de propagación equivalente. Lo que de verdad decide la experiencia está fuera del grafo: si el render es de grano fino o por componente, si hay compilador o no, cómo se gestiona el ciclo de vida, y cómo encaja todo con el SSR y lo asíncrono. Mira ahí, no al motor.

MobX, Angular y Preact

Tres puntos de comparación, cada uno revelador de una decisión distinta:

MobX es el abuelo del linaje. Rastrea con proxies: lees pedido.total como una propiedad normal y una capa de interceptores registra la dependencia contra su trackingDerivation. Es transparente —no hay paréntesis— pero paga un precio: un runtime mayor, la posibilidad de perder la reactividad al desestructurar un observable, y una observabilidad profunda que hay que domesticar con action y runInAction. Sus computed son perezosos de verdad: se suspenden por completo cuando nadie los observa y despiertan al primer lector. MobX es agnóstico de la vista —no renderiza nada por sí mismo—; emparejado con React, el envoltorio observer suscribe el componente y fuerza un re-render entero cuando cambia algo que leyó en su último render.

Los signals de Angular —estables desde Angular 17, centrales en su modo zoneless de 2026— leen como funciones, count(), igual que Solid. Sus computed son perezosos, el grafo es glitch-free con versionado, y su modelo producer/consumer se parece asombrosamente a las sources y observers con slots que ya conoces: no es coincidencia, sus autores colaboran con los de Solid en la propuesta de TC39. La diferencia está en el destino de la arista: un signal leído en una plantilla marca ese componente para revisión por la detección de cambios. La granularidad, por ahora, sigue siendo el componente, no el nodo del DOM. La limpieza se ata al contexto de inyección y al DestroyRef, no a un árbol de propiedad propio.

Los signals de Preact leen por propiedad: count.value. Por dentro usan una lista doblemente enlazada de nodos con contadores de versión —un consumidor comprueba si la versión de alguna fuente avanzó desde su último cálculo para decidir si recomputa—, y son perezosos y glitch-free. Su gracia es doble: en Preact, pasar un signal directo al JSX crea un enlace de texto fino, sin re-render, muy al estilo de Solid; pero un signal leído en el cuerpo de un componente, o a través del adaptador de React, dispara el re-render de ese componente. La misma librería ofrece las dos granularidades según dónde leas.

flowchart TB
G[grafo de signals: mismo algoritmo] --> R1[Solid: enlace fino al DOM]
G --> R2[Angular: revisa el componente]
G --> R3[Preact React: re-render del componente]
G --> R4[MobX React: re-render del componente]
style G fill:#89b4fa,color:#11111b
style R1 fill:#a6e3a1,color:#11111b
style R2 fill:#fab387,color:#11111b
style R3 fill:#fab387,color:#11111b
style R4 fill:#fab387,color:#11111b

El diagrama cuenta la historia entera: el mismo grafo arriba, cuatro destinos distintos abajo. Tres de los cuatro terminan volviendo a ejecutar una función de componente. Solo uno no. Y la lista no acaba en cuatro: la reactividad de Vueref y computed sobre proxies con versiones— es el mismo grafo; las runes de Svelte 5$state, $derived— son el mismo grafo que su compilador cablea; los signals de Qwik, serializables para reanudar la app en el cliente sin re-ejecutarla, son el mismo grafo hecho persistible. Toda la industria rema hacia la misma orilla algorítmica.

Si destilas las diferencias que quedan, se reducen a dos ejes: cómo se lee y qué se re-ejecuta al cambiar. Cada elección tiene su precio:

🪞

Proxy transparente (MobX, Vue)

Lectura sin ceremonia, pedido.total, pero runtime mayor, reactividad que se pierde al desestructurar y observabilidad profunda que domesticar. Mágico hasta que deja de serlo.

📞

Lectura por función (Solid, Angular)

count() pide un paréntesis, pero el signal es un valor de primera clase: se pasa, se deriva y se compone sin perder reactividad al copiarlo. Explícito y portable.

🏷️

Propiedad value (Preact)

count.value a medio camino: un descriptor que intercepta sin proxy. Claro y ligero, aunque obliga a recordar el sufijo en cada acceso al valor.

🎯

Grano del render

Componente entero —React con MobX o Preact, Angular por ahora— frente a nodo del DOM —Solid, y Preact en su JSX—. El mismo grafo, muy distinta cantidad de trabajo por cambio.

El estándar que viene: TC39

En 2024 se presentó ante TC39 —el comité que gobierna JavaScript— una propuesta para llevar los signals al lenguaje mismo. No la firma un framework: la impulsa un grupo de trabajo con autores de Solid, Angular, Preact, Vue, MobX, Svelte y Qwik sentados a la misma mesa. Que rivales históricos acuerden una API común es la prueba más contundente de que el algoritmo del grafo está resuelto y ya no es terreno de ventaja competitiva.

La propuesta expone dos primitivos, Signal.State y Signal.Computed, más un Signal.subtle.Watcher de bajo nivel sobre el que cada framework construye sus propios efectos y su propia integración con el render. Por dentro es, línea por línea, lo que has estudiado en este nivel: un grafo con coloreado de nodos —limpio, a-comprobar, sucio—, propagación push-pull, y evaluación perezosa glitch-free con resolución hacia arriba. Los nombres cambian; el motor es exactamente el tuyo.

ℹ️
Qué significaría un signal nativo

Si la propuesta avanza, un día Signal.State vivirá en el motor de JavaScript como hoy viven Map o Promise. Los frameworks dejarían de reimplementar el grafo y podrían compartir uno solo, interoperable: un signal creado por una librería sería legible y observable por otra. Solid no perdería su ventaja por ello —nunca residió en el motor, sino en el renderizador conectado al grafo—, pero podría apoyarse en un sustrato común y adelgazar su núcleo. Estudiar hoy las tripas de Solid es, en buena medida, estudiar el futuro estándar de la reactividad en la web: cuando aterrice, ya lo entenderás.

Qué hace único a Solid

La diferencia de Solid no es cómo lee un signal ni cómo colorea su grafo —eso lo comparte con todos—, sino qué hay al otro extremo de la arista. En los demás, el signal es una capa de gestión de estado encima de un renderizador que sigue pensando en componentes y en re-ejecutarlos. En Solid, el grafo reactivo es el renderizador:

  • Sin virtual DOM y sin re-render. Una arista del grafo no termina en “marca este componente sucio”, sino en un efecto que escribe un nodo de texto o un atributo concreto. El cambio de un signal actualiza exactamente el trozo de DOM que depende de él, sin comparar árboles ni reconciliar.
  • El componente corre una vez. No hay función de componente que se re-ejecute. El cuerpo se ejecuta al crear, teje el grafo, y desaparece. La “vida” ocurre en los efectos, no en repeticiones del render.
  • El compilador cose grafo y DOM. El compilador de JSX de Solid convierte tus plantillas en clones de DOM más efectos finos que enlazan cada signal a su punto de inserción exacto. Reactividad y renderizado están co-diseñados, no pegados uno encima del otro.
  • Árbol de propiedad para el cleanup. Cada nodo vive en un Owner; al destruirse el ámbito, sus efectos y suscripciones se limpian solos. La disposición es estructural, no manual como los disposers que has de recordar en MobX.
  • Lectura por función, no por proxy. Un signal es una función que puedes pasar, devolver, guardar y derivar sin perder reactividad al copiarla. Comparte esta virtud con Angular y con la propuesta de TC39, y la contrasta con los proxies de MobX, que se rompen al desestructurar.

Suma esos cinco rasgos y verás que no son cinco decisiones independientes, sino cinco consecuencias de una sola: que el grafo no informe a un renderizador, sino que sea el renderizador. Quita el virtual DOM y el re-render y el componente ya no necesita volver a ejecutarse; si no se re-ejecuta, hace falta un compilador que ate cada signal a su punto exacto del DOM; y para saber cuándo desmontar esos enlaces, hace falta un árbol de propiedad. Una elección arrastra a las demás. Ahí está el coste, también: Solid necesita su compilador y su modelo de propiedad, mientras que MobX o Preact-React se acoplan a un renderizador que ya existía. Es el precio de la coherencia.

Para verlo con las manos, mira en qué convierte el compilador un fragmento de JSX:

// Lo que escribes:
<span class={activo() ? "on" : "off"}>{contador()}</span>

// Lo que el compilador genera, conceptualmente: un clon del elemento
// mas un efecto fino por cada binding reactivo, cada uno atado a su punto:
const el = plantilla();   // clona el span una sola vez, sin reactividad
createRenderEffect(() => setAttribute(el, "class", activo() ? "on" : "off"));
createRenderEffect(() => (el.firstChild.data = contador()));

Cada createRenderEffect es una hoja del grafo enganchada a un punto concreto del DOM. Cuando contador cambia, se re-ejecuta solo el segundo efecto y solo se reescribe ese nodo de texto; el atributo class ni se toca. No hay componente que vuelva a correr, no hay árbol que comparar: la arista termina en el.firstChild, y ahí muere el trabajo. Compara con el modelo de re-render, donde el mismo cambio re-ejecuta la función entera del componente y confía en un diff para descubrir que, de todo el árbol, solo cambió un carácter. Esa es, hecha código, la diferencia entre usar signals y ser el renderizador.

Convergen en el algoritmo, divergen en la integracion

Aquí está la síntesis de todo el nivel. El grafo reactivo —nodos, listas bidireccionales, oyente global, push-pull coloreado, glitch-freedom— se ha convertido en 2026 en un bien común de la industria: MobX lo inventó, Preact y Angular lo perfeccionaron, la propuesta de TC39 lo va a estandarizar para que un día viva en el propio JavaScript. Si comparas motores de signals, todos son parientes cercanos. Lo que distingue a Solid no es, por tanto, su motor —por excelente que sea— sino la decisión arquitectónica de no poner ese motor al servicio de un renderizador de componentes, sino de convertirlo en el renderizador. En React con MobX, o con el adaptador de Preact, un signal que cambia acaba diciendo “vuelve a ejecutar esta función de componente y compara el resultado”. En Solid, un signal que cambia dice “escribe este valor en este nodo del DOM”. Esa es la diferencia entre usar signals para decidir cuándo re-renderizar y usar signals para ser la actualización. Todo lo demás —que el componente corra una vez, que no haya virtual DOM, que no necesites useMemo ni listas de dependencias, que el rendimiento sea predecible sin trucos— se deduce de ahí. Los otros adoptaron los signals como una mejora de su modelo; Solid construyó su modelo entero alrededor de ellos. Por eso, cuando entiendes el grafo de Solid por dentro, no entiendes solo un framework: entiendes el algoritmo hacia el que converge todo el frontend, y ves con claridad qué significa llevarlo hasta sus últimas consecuencias.

⚔️ Ubica cada sistema en el mapa
  1. Para MobX, Angular, Preact y Solid, escribe una tabla con tres columnas: sintaxis de lectura (proxy, .value o función), estrategia de render (re-render de componente o enlace fino) y modelo de cleanup.
  2. Explica por qué “el componente corre una vez” es incompatible con el modelo de React pero natural en Solid, y cómo se relaciona con la ausencia de virtual DOM.
  3. Toma un componente que hayas escrito en React con MobX o con useState y señala qué parte de su complejidad era gestionar re-renders. Eso es lo que el modelo de Solid elimina de raíz.
  4. Lee por encima la propuesta de signals de TC39 y localiza Signal.State, Signal.Computed y el coloreado de nodos; relaciónalos con el CLEAN, PENDING y STALE de las lecciones 2 y 3.
  5. Prueba a desestructurar un observable de MobX y comprueba que pierdes la reactividad; explica por qué un signal-función de Solid no sufre ese problema.
  6. Argumenta, en un párrafo, la tesis central: la ventaja de Solid no está en el algoritmo del grafo —común a todos— sino en qué conecta al otro extremo de sus aristas.