El JSX de Solid compila a DOM real
Por qué en Solid no hay createElement ni virtual DOM: el compilador parte cada plantilla en una porción estática clonable y unas pocas ligaduras reactivas, generando template(), insert() y effect(). La visión de lo que produce dom-expressions y la delegación de eventos.
Dos filosofías, la misma sintaxis
En React, el JSX es azúcar sobre llamadas a función. <div class="a">{x}</div> se transpila a createElement("div", { class: "a" }, x), que devuelve un objeto plano —un elemento de React—. En cada render se reconstruye ese árbol de objetos, se compara con el anterior y se aplican al DOM solo las diferencias. El virtual DOM es el precio que React paga para que puedas describir la UI como una función pura del estado. Ese contrato —la UI como función del estado— es elegante y tiene un coste: cada cambio obliga a reconstruir y recomparar, y todo el aparato de memo, useMemo y useCallback nace para mitigarlo.
Solid rechaza esa premisa. Su compilador no traduce JSX a llamadas de creación de nodos en tiempo de ejecución: lo analiza en tiempo de build y emite código especializado para ese fragmento concreto. El JSX no es un valor que se recalcula; es una plantilla que se instancia una vez y se parchea quirúrgicamente después.
Lo que genera el compilador
Tomemos un fragmento con parte fija y parte dinámica y miremos su salida conceptual. El compilador de Solid —babel-plugin-jsx-dom-expressions, envuelto por babel-preset-solid— lo divide en dos mundos.
// lo que escribesconst saludo = <div class="card">Hola, {nombre()}</div>;// lo que genera, en esenciaimport { template as _tmpl, insert as _insert } from "solid-js/web";// 1) la porcion ESTATICA se hornea en una plantilla clonableconst _tpl = _tmpl(`<div class="card">Hola, `);const saludo = (() => { const _el = _tpl(); // clona el nodo, no lo reconstruye _insert(_el, nombre, null); // 2) la parte DINAMICA se liga aparte return _el;})();Hay dos ideas en ese código. Primero, template() recibe una cadena HTML y crea una sola vez un elemento template del navegador; cada instancia posterior es un cloneNode barato, que es la operación de creación de DOM más rápida que existe. Segundo, lo que varía —nombre()— no vive en la cadena: se conecta con insert(), que crea internamente un efecto de render suscrito a ese signal. La porción estática se clona; la dinámica se liga.
Cuando el binding es un atributo reactivo, aparece effect() de forma explícita, envolviendo solo la asignación que puede cambiar:
const boton = <button class={clase()} disabled={cargando()}>Enviar</button>;const _tpl = _tmpl(`<button>Enviar`);const boton = (() => { const _el = _tpl(); _effect(() => { _className(_el, clase()); // se re-ejecuta si clase cambia _el.disabled = cargando(); // ...o si cargando cambia }); return _el;})();Los componentes se compilan aparte
Los elementos nativos —div, button— se hornean en plantillas, pero un componente —una etiqueta con inicial mayúscula, como <App /> o <Boton color={c()} />— no es un nodo del DOM, sino una función tuya. El compilador no puede clonarlo, así que lo trata distinto: lo convierte en una llamada a createComponent con un objeto de props de getters reactivos.
const ui = <Boton color={color()}>Ir</Boton>;const ui = _createComponent(Boton, { get color() { return color(); }, // getter: preserva la reactividad children: "Ir",});Dos cosas se revelan. Primero, cada prop es un getter, no un valor copiado: leer props.color dentro del componente se suscribe a la señal, y destructurar la firma —que evalúa el getter una vez— rompe esa suscripción. Segundo, createComponent no clona ni compara: invoca tu función una sola vez, dentro del contexto reactivo del padre, y devuelve lo que produzca. Componer en Solid es, al pie de la letra, funciones que se llaman entre sí y devuelven DOM real.
Esto alcanza al control de flujo. Show, For, Switch o Dynamic no son sintaxis mágica: son componentes normales que también compilan a createComponent y reciben sus condiciones y colecciones como getters. Por eso un <Show when={visible()}> reacciona sin re-render del padre —el propio Show observa when y monta o desmonta su contenido— e incluso un fragmento <>...</> es solo un array de nodos que el compilador ensambla, sin envoltorio ni coste.
// Show es un componente: recibe when como getter y reacciona solo<Show when={sesion()} fallback={<Enlace />}> <Panel /></Show>Sin VDOM: consecuencias que se notan
Que no exista un árbol virtual no es un detalle de implementación; reordena toda la economía del framework.
La delegación de eventos merece una nota. Cuando escribes onClick, el compilador no hace addEventListener en ese botón: registra el tipo de evento una vez en el documento con delegateEvents(["click"]) y guarda tu manejador como una propiedad del nodo. Un único listener global despacha todos los clics siguiendo la jerarquía. Si necesitas un listener nativo directo —por ejemplo para eventos que no se propagan— usas la forma on:click, que Solid ata literalmente con addEventListener.
Todo esto dibuja un modelo de coste distinto. En un framework con VDOM, el trabajo de una actualización crece con el tamaño de la vista, porque hay que recorrer y comparar el árbol para descubrir qué cambió. En Solid crece con el tamaño del cambio: un signal que muta despierta a sus suscriptores directos y a nadie más, dé igual que la página tenga diez nodos o diez mil. La vista puede ser enorme y las actualizaciones seguir siendo diminutas, porque el grafo ya sabe, por construcción, quién depende de qué. Esa es la razón última de que Solid rinda sin que lo optimices: no es que compare más rápido, es que no compara.