wandres.dev
LOS MOTORES COMPARADOS · Solid, Vue, Angular, Svelte, Preact

Solid: el grafo explícito

Las ocho decisiones de diseño de Solid, el motor que expone su grafo con menos intermediarios: arrays con índices cruzados, marcado en dos fases, efectos síncronos y árbol de dueños de primera clase.

⏱ 17 min

Solid es el motor de esta familia que menos esconde su maquinaria. Sus primitivas se corresponden casi uno a uno con los nodos del grafo, su árbol de dueños es una API pública, y su modelo de propagación es exactamente el marcado en dos fases de los niveles 4 y 5 sin adornos. Eso lo convierte en el mejor punto de partida para leer código fuente de un motor real, y también en el que más responsabilidad deja al programador.

🎯 Al terminar esta lección sabrás
  • Situar las decisiones de Solid en las ocho dimensiones del track.
  • Reconocer sus estructuras internas y su vocabulario.
  • Entender qué optimiza su elección de efectos síncronos.
  • Identificar qué paga a cambio de su explicitud.

Las ocho decisiones

Representación del grafo. Arrays paralelos con índices cruzados, tal como vimos en el nivel 2. Cada nodo tiene sources y sourceSlots, observers y observerSlots, creados de forma perezosa. Alta y baja en tiempo constante, iteración sobre memoria contigua.

Descubrimiento de dependencias. Tracking automático con una variable de módulo llamada Listener. Ofrece untrack para leer sin rastrear y el helper on para declarar dependencias explícitamente.

Propagación. Marcado en dos fases con dos estados no limpios, STALE y PENDING, y resolución hacia arriba desde la lectura. Sin orden topológico explícito.

Consistencia. Glitch-free por el mecanismo anterior, con dos colas internas separadas para computaciones puras y efectos.

Memoización. createMemo, con opción equals que admite una función a medida o false para desactivar la comparación por completo.

Ciclo de vida. Árbol de dueños de primera clase: createRoot, onCleanup, getOwner, runWithOwner. Es el motor donde este mecanismo es más visible y más completo.

Planificación. Síncrona por defecto. batch para agrupar explícitamente. createRenderEffect para el trabajo que debe ocurrir durante el renderizado y createEffect para el posterior.

Modelo de estado. Los dos: createSignal para lo atómico y createStore para lo profundo, con proxies de solo lectura y actualización por camino, más produce y reconcile.

Lo que lo distingue

La señal es una pareja de funciones. createSignal devuelve un array con el getter y el setter, y eso tiene una consecuencia ergonómica notable: puedes pasar el getter sin conceder permiso de escritura, y la lectura es visiblemente una llamada.

const [contador, setContador] = createSignal(0);
mostrar(contador);              // paso la capacidad de leer, no la de escribir
setContador(c => c + 1);

El componente corre una vez. No hay re-render. Toda la reactividad vive en las expresiones de la plantilla, que el compilador convierte en efectos individuales. Es la consecuencia directa del modelo de grano fino del nivel 1, llevada hasta el final.

El compilador de JSX genera plantillas. En vez de producir un árbol descriptivo, el compilador emite una plantilla HTML que se clona y un conjunto de efectos que rellenan los huecos variables. El nivel 11 lo desarrolla.

El árbol de dueños es API pública. Puedes capturar un dueño y volver a él, crear raíces desacopladas y registrar limpiezas desde cualquier profundidad. Pocos motores exponen esto tan directamente.

flowchart TB
S[createSignal] --> M[createMemo]
M --> E[createEffect]
S --> R[efecto de render generado por el compilador]
M --> R
R --> D[nodo del DOM concreto]
style S fill:#89b4fa,color:#11111b
style M fill:#cba6f7,color:#11111b
style E fill:#a6e3a1,color:#11111b
style R fill:#a6e3a1,color:#11111b
style D fill:#94e2d5,color:#11111b

Qué optimiza la ejecución síncrona

Solid ejecuta los efectos al cerrar el lote, de forma síncrona. Es una decisión coherente con el resto de su diseño y compra tres cosas.

Trazas de pila completas. Un fallo en un efecto muestra la escritura que lo provocó. En un motor asíncrono la pila empieza en el vaciado.

Estado al día al volver. Escribes una señal y la línea siguiente ya ve el DOM actualizado, sin esperar a ningún tick.

Un modelo mental sin momentos ocultos. No hay que preguntarse cuándo corre algo: corre ahora. Para un motor que se presenta como transparente, esa coherencia importa.

El precio es que hay que agrupar a mano cuando escribes varias señales fuera de un contexto ya agrupado, y que las escrituras del DOM se reparten por el turno en vez de agruparse antes del pintado. Solid mitiga lo segundo con createRenderEffect, que corre en la fase de renderizado.

ℹ️
El helper on es la valvula de escape del tracking

Cuando quieres dependencias declaradas en vez de descubiertas, on envuelve una computación y le fija sus fuentes, con la opción de diferir la primera ejecución. Es la vía de Solid al modelo del nivel 2, y existe porque hay casos donde el redescubrimiento continuo no compensa: cuerpos caros con dependencias estables, o efectos que solo deben reaccionar a una de sus lecturas.

Lo que paga

Explicitud como carga cognitiva. Hay que saber que un getter es una función y que llamarla dentro del ámbito correcto importa. Hay que saber que el componente corre una vez. Hay que saber cuándo agrupar. Son cosas que un motor con más magia esconde, y esconderlas tiene valor para quien no quiere pensar en ellas.

La trampa de las propiedades de componente. Como el componente corre una vez, desestructurar sus propiedades congela los valores. El compilador transforma las propiedades en un objeto con getters, así que acceder a ellas funciona, pero desestructurar no. Es la misma trampa del nivel 9 en otro sitio.

Menos herramientas que los ecosistemas grandes. Es una consecuencia del tamaño de la comunidad, no del diseño, pero es real a la hora de elegir.

Solid es la referencia porque su codigo se parece a su modelo

Si vas a leer el código fuente de un solo motor reactivo en tu vida, que sea este, y no por ninguna superioridad técnica sino por una propiedad pedagógica concreta: la distancia entre su modelo conceptual y su implementación es mínima. createSignal crea un SignalState. createMemo crea un Computation con la bandera de pureza activada. createEffect crea uno con la bandera desactivada. Listener es el observador actual del nivel 3. Owner es el dueño del nivel 7. STALE y PENDING son el sucio y el quizá sucio del nivel 4. No hay ninguna capa de traducción entre lo que has aprendido en este track y lo que vas a leer en signal.ts. Otros motores tienen abstracciones intermedias que hay que atravesar antes de reconocer la misma máquina, y aunque esas abstracciones existan por buenas razones —soportar más casos, integrarse con un framework mayor— hacen que el aprendizaje sea más lento. Hay además una implicación que va más allá de leer código: como el modelo y la implementación coinciden, razonar sobre el rendimiento de una aplicación en Solid es razonar sobre el grafo, sin traducciones ni sorpresas. Cuando el perfil dice que algo se ejecuta demasiado, la causa está en una arista que puedes señalar. Esa propiedad —que la abstracción no filtre— es rara y vale mucho, y es la razón por la que Solid funciona tan bien como motor de referencia para entender a todos los demás.

⚔️ Lee el nucleo
  1. Clona el repositorio de Solid y abre el fichero del núcleo reactivo.
  2. Localiza las cinco piezas del nivel 0 y anota el nombre exacto de cada una.
  3. Encuentra la función de lectura de señal y compárala con la de la lección 2 del nivel 2.
  4. Localiza las dos colas de actualización y comprueba en qué orden se vacían.