La reactividad de grano fino
Qué significa exactamente que solo se actualice el nodo que depende de un signal: el átomo reactivo, el grafo de sources y observers, y por qué Solid no re-ejecuta tu componente.
La frase que define a Solid es “reactividad de grano fino”, y casi nadie la explica con precisión. No significa “rápido”. Significa algo mecánico y exacto: cuando un valor cambia, se vuelve a ejecutar únicamente el trozo de código que lee ese valor, y ese trozo actualiza únicamente el nodo del DOM que le corresponde. Ni un byte de trabajo de más. Entender ese “exactamente” es entender Solid entero.
- Definir con rigor qué es la reactividad de grano fino.
- Ver por qué solo se actualiza el nodo que lee un signal.
- Entender el grafo reactivo: sources, observers y propagación.
- Contrastar el modelo con el de re-render de arriba abajo.
El átomo reactivo: un signal es una suscripción
Un signal es el primitivo fundamental. createSignal devuelve una pareja: un getter (una función) y un setter. Leer el getter no es “leer una variable”: es declarar una suscripción en el contexto reactivo donde lo lees.
import { createSignal, createEffect } from "solid-js";
function Contador() {
const [count, setCount] = createSignal(0);
const [tema, setTema] = createSignal("azul");
// Este efecto lee count(): se suscribe SOLO a count.
createEffect(() => console.log("count =", count()));
return (
<button class={tema()} onClick={() => setCount(count() + 1)}>
Clics: {count()}
</button>
);
}
El cuerpo de Contador corre una sola vez. Cuando llamas a setCount, Solid no vuelve a invocar la función del componente: consulta quién leyó count() y re-ejecuta solo a esos suscriptores. Aquí son dos: el createEffect del console.log y la expresión {count()} del JSX, que por dentro es su propio efecto ligado a un único nodo de texto. La class que lee tema() no se toca, porque nadie que dependa de tema cambió.
Grano fino: reacciona el nodo, no el componente
Esta es la diferencia que hay que interiorizar. En un modelo de re-render, cambiar un estado re-ejecuta el componente completo y produce una descripción nueva de toda su UI. En Solid, cambiar un signal dispara solo las computaciones suscritas, y cada una hace la operación mínima sobre el DOM real.
flowchart LR S[Signal count] -->|se suscribe| E1[Efecto del texto] S -->|se suscribe| E2[Efecto del console] T[Signal tema] -->|se suscribe| E3[Efecto de la clase] E1 -->|actualiza| N1[Nodo de texto Clics] E3 -->|actualiza| N2[Atributo class] style S fill:#89b4fa,color:#11111b style T fill:#f9e2af,color:#11111b style E1 fill:#94e2d5,color:#11111b style E2 fill:#94e2d5,color:#11111b style E3 fill:#94e2d5,color:#11111b style N1 fill:#a6e3a1,color:#11111b style N2 fill:#a6e3a1,color:#11111b
La granularidad es la del binding, no la del componente ni la de un <div> entero. Si un componente pinta cien campos y cambia uno, en Solid se actualiza exactamente ese campo: no hay una función que reconstruya los cien para que un diff descubra cuál difiere. El descubrimiento ya ocurrió cuando leíste el getter.
Conviene subrayar qué NO ocurre en este proceso. No se construye una representación intermedia de la interfaz. No se compara nada contra un estado anterior. No se marca el componente como “sucio” para revisarlo en un ciclo posterior. El setter localiza la lista de suscriptores del signal y ejecuta exactamente esos; el resto del programa ni se entera de que algo cambió.
Esa ausencia de trabajo especulativo —hacer cosas “por si acaso” para luego descartarlas— es la esencia del grano fino, y es lo que separa a Solid de cualquier sistema que primero produce una descripción completa de la UI y después decide qué parte de ella aplicar.
count() solo crea una dependencia si se lee dentro de un efecto, un createMemo o el JSX. Si lo lees en un manejador de evento como onClick, obtienes el valor actual sin suscribirte. Por eso en el ejemplo el onClick puede leer count() sin convertirse en dependiente de sí mismo.
El grafo: sources, observers y propagación
Por debajo, Solid mantiene un grafo dirigido. Los sources son lo que se puede leer (signals y memos). Los observers son lo que reacciona (efectos y memos). Cuando un observer lee un source durante su ejecución, Solid graba la arista en ambos sentidos: el source sabe quién lo observa y el observer sabe de qué depende.
Source
Un valor observable: signal o memo. Al escribirlo, notifica a sus observers.
Observer
Una computación que se re-ejecuta: createEffect o createMemo. Al correr, registra sus sources.
Arista dinámica
Las dependencias se recalculan en cada ejecución: si un if deja de leer un source, la arista desaparece.
Propagación
Escribir un source marca a sus observers y los ejecuta; el trabajo es proporcional a lo que cambió, no al tamaño del árbol.
Las aristas son dinámicas: se reconstruyen en cada ejecución del observer. Si un efecto solo lee b() cuando a() es verdadero, deja de depender de b en cuanto a se vuelve falso. No hay array de dependencias que mantener a mano: el grafo se dibuja solo a partir de las lecturas reales.
import { createSignal, createEffect } from "solid-js";
const [detallado, setDetallado] = createSignal(true);
const [valor, setValor] = createSignal(10);
// Cuando detallado() es false, el efecto deja de depender de valor().
createEffect(() => {
if (detallado()) console.log("detalle:", valor());
else console.log("resumen");
});
Un createMemo ocupa una posición doble en el grafo: es observer de sus fuentes y, a la vez, source para quien lo lee. Encadenando signal a memo a efecto construyes derivaciones cacheadas, y Solid solo recalcula un memo cuando de verdad cambia alguna de sus fuentes. Ese encadenamiento es lo que deja componer lógica reactiva compleja sin recalcular de más ni ensuciar el DOM con pasos intermedios.
Al escribir un source, Solid propaga el cambio de forma síncrona y en orden topológico: un observer nunca se ejecuta leyendo una fuente a medio actualizar, así que no observas valores intermedios inconsistentes (los llamados glitches). Si necesitas aplicar varias escrituras como una única propagación, batch las agrupa y notifica a los observers una sola vez al final.
Grano fino quiere decir que la unidad de reactividad es la lectura individual de un source, no el componente. Cuando escribes un signal, Solid no pregunta “¿qué cambió en la UI?” — ya lo sabe, porque durante la lectura registró exactamente qué computaciones dependían de ese valor y qué nodo del DOM produce cada una. La actualización es una operación quirúrgica: recorrer la lista de observers de ese source y ejecutarlos. No hay recorrido de árbol, no hay comparación, no hay reconstrucción. Por eso “solo se actualiza el nodo que depende del signal” no es una metáfora ni una optimización: es literalmente cómo está construido el sistema. La reactividad no es una capa sobre el render; es el render.
Por qué esto cambia el modelo mental
Si vienes de un modelo de re-render, tu instinto es preguntar “¿cuándo se vuelve a renderizar esto?”. En Solid la pregunta correcta es “¿qué lee este valor?”. El estado no dispara renders: dispara actualizaciones puntuales de los nodos que lo leyeron. Los memoizadores manuales, la comparación de props y las claves para evitar trabajo repetido dejan de ser necesarios, porque nunca hubo trabajo repetido que evitar.
Esa economía tiene una consecuencia práctica enorme: el rendimiento deja de ser algo que optimizas después y pasa a ser el comportamiento por defecto. No existe una versión lenta que haya que arreglar con memo; la primera versión ya hace el trabajo mínimo. Tu esfuerzo se traslada de “evitar renders” a “modelar bien las dependencias entre datos”, que es donde de verdad vive la complejidad de una interfaz.
La pregunta cambia
De “¿cuándo se re-renderiza?” a “¿qué lee este valor?”. El grafo responde por ti, siempre.
Sobra el andamiaje
Ni memo de componente, ni comparación de props, ni claves para saltar un trabajo que no ocurre.
Síncrono por defecto
Al escribir un signal, el nodo afectado se actualiza en el acto, sin esperar a un ciclo de render futuro.
Este es, en el fondo, el argumento entero de Solid en una sola frase: si el sistema sabe con exactitud qué depende de qué, no necesita adivinar, y todo lo que otros frameworks construyen para adivinar mejor —heurísticas de diffing, memoización, claves de lista— se vuelve innecesario. La reactividad de grano fino no es una técnica de optimización que aplicas encima: es la decisión de no generar, para empezar, el trabajo que después habría que optimizar.
- Crea un componente con dos signals,
ayb, y uncreateEffectque solo leaa(). - Pon un
console.logen el cuerpo del componente y otro dentro del efecto. - Actualiza
bvarias veces: observa que el log del cuerpo no vuelve a salir y el del efecto tampoco. - Actualiza
a: solo se re-ejecuta el efecto. Acabas de ver el grano fino con tus ojos. - Envuelve dos escrituras seguidas en
batchy confirma que los observers corren una sola vez, no dos.