Sin virtual DOM ni re-render
Cómo Solid parte del JSX y actualiza el DOM directamente: el compilador que convierte tu markup en plantillas y efectos, frente a la reconciliación de árboles virtuales.
El JSX de Solid se parece al de React hasta que compilas: entonces se revela que no describe un árbol virtual, sino instrucciones para construir DOM real y coserle efectos. Solid no tiene virtual DOM, no reconcilia y no re-renderiza. El “diff” que otros frameworks hacen en cada frame, Solid lo hizo una vez, en tu build. Esta lección abre el capó.
- Distinguir las dos rutas de un framework hacia el DOM.
- Ver en qué compila el JSX de Solid: plantillas y efectos.
- Entender qué es la reconciliación y por qué Solid la evita.
- Medir el coste de runtime que desaparece sin virtual DOM.
Dos formas de llegar al DOM
Todo framework de UI resuelve el mismo problema: mantener el DOM en sincronía con el estado. Hay dos estrategias de fondo, y definen todo lo demás.
flowchart TD J[Tu JSX] --> RV[Ruta virtual DOM] J --> RC[Ruta compilada] RV --> V1[createElement en cada render] V1 --> V2[Arbol virtual nuevo] V2 --> V3[Diff con el arbol viejo] V3 --> V4[Patch al DOM real] RC --> C1[Compilador transforma el JSX] C1 --> C2[Plantilla DOM clonable] C2 --> C3[Efectos por cada parte dinamica] C3 --> C4[Actualiza el nodo exacto] style RV fill:#f38ba8,color:#11111b style RC fill:#a6e3a1,color:#11111b style V4 fill:#fab387,color:#11111b style C4 fill:#89b4fa,color:#11111b
La ruta del virtual DOM trata el JSX como datos: en cada render construye una descripción nueva de la UI y la compara con la anterior para deducir qué tocar. La ruta de Solid trata el JSX como código: un compilador lo convierte, antes de que el navegador lo vea, en una plantilla estática más un puñado de efectos que actualizan las partes que cambian.
La distinción “datos contra código” no es retórica: decide quién trabaja y cuándo. Tratar el JSX como datos aplaza toda la decisión al runtime, que debe recalcular la descripción y compararla en cada cambio. Tratarlo como código adelanta esa decisión al compilador, que emite instrucciones específicas para cada parte dinámica. Es la diferencia entre interpretar una partitura en directo cada vez y grabarla una sola vez en un rollo de pianola.
Svelte también compila, y de ahí sale buena parte de su velocidad. La diferencia está en el destino: Svelte 5 compila hacia un sistema de signals de grano fino muy parecido al de Solid, mientras que un framework con virtual DOM compila, si acaso, hacia llamadas que aún construyen y comparan árboles en runtime. El paradigma de reactividad al que apuntas importa más que el mero hecho de compilar.
Qué compila Solid: del JSX a instrucciones
Este es el corazón. Tomemos un fragmento y veamos su forma compilada, de manera conceptual.
// Lo que escribes
function Saludo(props) {
return <div class={props.tema}>Hola, {props.nombre()}</div>;
}
// En lo que compila Solid, de forma simplificada
const _tmpl = template(`<div>Hola, </div>`);
function Saludo(props) {
const el = _tmpl(); // clona un nodo real, una vez
insert(el, props.nombre); // efecto: mantiene el texto en sync
effect(() => setAttribute(el, "class", props.tema)); // efecto para la clase
return el; // devuelve un nodo del DOM, no un objeto describiendolo
}
Fíjate en tres cosas. Primero, la parte estática (<div>Hola, ) se convierte en una plantilla que se clona con cloneNode, la operación más barata que ofrece el navegador. Segundo, cada parte dinámica (props.nombre() y props.tema) genera su propio efecto de grano fino, ligado a un nodo o atributo concreto. Tercero, el componente devuelve un nodo real del DOM, no un árbol virtual que alguien tenga que interpretar después.
La consecuencia es que el tamaño de tu componente no impone un coste de runtime recurrente. Cien nodos estáticos son una plantilla que se clona una vez; solo las partes que leen un signal generan efectos. La parte inmutable de tu interfaz es, literalmente, gratis tras el primer montaje, mientras que en un modelo de virtual DOM cada uno de esos cien nodos se re-describe y se re-compara en cada render aunque nunca cambie.
Solid no interpreta tu JSX en runtime: lo transforma en build con babel-plugin-jsx-dom-expressions, la base sobre la que se apoya el renderer. Por eso el runtime de Solid puede ser diminuto (del orden de 7 kB): no incluye ni un reconciliador ni un motor de diffing, porque ese trabajo ya está resuelto en el código generado.
Reconciliación: el trabajo que Solid no hace
La reconciliación es el algoritmo que, dado un árbol virtual nuevo y uno viejo, calcula el conjunto mínimo de mutaciones del DOM. Es una idea brillante y costosa: exige recrear la descripción de la UI en cada cambio y recorrerla entera para compararla, aunque solo haya cambiado un carácter. Las claves de lista, la memoización de componentes y la comparación de props existen, en buena medida, para abaratar ese recorrido.
Reconstruir
El modelo virtual crea un árbol nuevo en cada render para tener algo con lo que comparar.
Comparar
Recorre ambos árboles buscando diferencias; el coste crece con el tamaño de la UI, no del cambio.
Parchear
Aplica al DOM real las diferencias encontradas. Solo este último paso toca la pantalla.
Solid: nada de esto
Sabe de antemano qué efecto actualiza qué nodo. Va directo al parcheo, sin reconstruir ni comparar.
Solid salta los dos primeros pasos por completo. No hay árbol que reconstruir porque nunca hubo un árbol virtual; no hay comparación porque el compilador ya asoció cada dato con su nodo. Solo queda la mutación puntual. Su análogo de la reconciliación, para listas, es <For>, que reordena nodos reales por referencia sin diffear estructura.
import { For, createSignal } from "solid-js";
function Lista() {
const [items] = createSignal([{ id: 1, txt: "a" }, { id: 2, txt: "b" }]);
return (
<ul>
<For each={items()}>{(item) => <li>{item.txt}</li>}</For>
</ul>
);
}
<For> es keyed por la referencia del objeto: si reordenas el array, Solid mueve los nodos <li> existentes en lugar de recrearlos, y solo crea o destruye los que de verdad aparecen o desaparecen. No hay un algoritmo de diff de árbol por detrás; hay un mapeo estable de cada elemento del array a su nodo real, que es justo lo que las claves de lista intentan aproximar en un modelo virtual.
La pregunta clave para comparar frameworks no es “¿cuánto tarda un render?”, sino “¿dónde ocurre el trabajo de averiguar qué cambió?”. En un modelo de virtual DOM, ese trabajo ocurre en runtime, en cada actualización, en el dispositivo del usuario, y crece con el tamaño de tu interfaz. En Solid ocurre una sola vez, en build, en tu máquina, y su coste no lo paga nadie en producción. Lo que llega al navegador ya es una máquina de estado precableada: “cuando este signal cambie, ejecuta este efecto sobre este nodo”. No queda deducción que hacer en runtime. Por eso Solid no necesita memo, ni comparaciones de props, ni trucos para saltarse renders: no existe el render de arriba abajo que optimizar. Eliminó la categoría entera del problema.
El coste que desaparece
Sin virtual DOM caen varias cosas a la vez: el peso del reconciliador en el bundle, la asignación de memoria de los árboles virtuales que el recolector luego limpia, y el trabajo de CPU proporcional al tamaño de la UI en cada cambio. Lo que queda es trabajo proporcional a lo que de verdad cambió. En los benchmarks de referencia, Solid se sitúa consistentemente a un pelo del DOM manual, y esa cercanía no es casualidad: hace casi lo mismo que escribirías tú a mano.
En SSR, hidratar suele significar reconstruir el árbol de componentes en el cliente para reenlazar la interactividad. Como Solid no re-ejecuta componentes ni mantiene un árbol virtual, su hidratación es más ligera: engancha los efectos a los nodos ya servidos por el servidor sin recrearlos. SolidStart explota esto para hidratar solo lo estrictamente necesario.
En conjunto, prescindir del virtual DOM no es una microoptimización: reordena de dónde sale el coste. El navegador del usuario recibe una máquina de estado ya resuelta y se limita a ejecutar mutaciones puntuales; el trabajo de deducir qué cambió se pagó una sola vez, en tu build, en tu máquina. Ese es el motivo mecánico —no una cuestión de fe ni de marketing— por el que Solid rinde como rinde.
Nada de esto significa que el virtual DOM fuera un error. Fue una idea brillante que resolvió el problema de su época: hacer manejable la actualización del DOM cuando escribirla a mano era tedioso y propenso a fallos. Durante más de una década fue el mejor compromiso disponible entre ergonomía y rendimiento.
Solid se apoya en esa herencia y da el siguiente paso: conserva la comodidad de describir la interfaz con JSX, pero paga el coste del análisis en compilación en lugar de en cada frame del navegador. Es evolución, no rechazo — la misma meta de siempre, alcanzada moviendo el trabajo a un momento en que no lo sufre el usuario.
- Abre el playground oficial de Solid en el navegador y escribe un componente con una parte estática y una dinámica.
- Cambia a la pestaña de salida compilada y localiza la llamada a
templatey los efectos generados. - Añade un segundo valor dinámico y observa cómo aparece otro efecto independiente, no un render mayor.
- Concluye: el “diff” ya está escrito en ese código. El navegador solo ejecuta mutaciones.
- Añade una lista con
<For>y comprueba que no aparece ningún algoritmo de diff de árbol, solo inserciones y movimientos de nodos reales.