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.
El algoritmo común
Casi todos los sistemas modernos comparten la misma columna vertebral, la que acabas de estudiar en Solid:
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 funcioncount.value; // Preact: acceso a una propiedad .valuepedido.total; // MobX: propiedad transparente interceptada por un proxycount.get(); // TC39 Signal.State: un metodo get explicitoBajo 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.
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.
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 Vue —ref 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:
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é 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 reactividadcreateRenderEffect(() => 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.