wandres.dev
SETUP Y JSX DE SOLID · no es React

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.

⏱ 13 min

El JSX de React y el de Solid se escriben casi igual, pero significan cosas opuestas. En React, <div> es una llamada a createElement que fabrica un objeto para diferenciarlo después. En Solid, ese mismo <div> desaparece en tiempo de compilación y se convierte en instrucciones que crean y actualizan nodos del DOM directamente. No hay objetos intermedios, no hay árbol virtual, no hay diffing. Entender qué genera el compilador es entender por qué Solid es rápido sin esforzarse.

🎯 Al terminar esta lección sabrás
  • Contrastar el modelo de React —createElement y VDOM— con el modelo compilado de Solid.
  • Ver qué genera el compilador dom-expressions: template(), insert() y effect().
  • Distinguir la porción estática clonable de las ligaduras reactivas de una plantilla.
  • Reconocer la delegación de eventos y por qué el coste de arranque es tan bajo.

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.

flowchart TD
subgraph React
  R1[JSX] --> R2[createElement] --> R3[arbol de objetos VDOM]
  R3 --> R4[diff con el arbol previo] --> R5[patch al DOM]
end
subgraph Solid
  S1[JSX] --> S2[compilador dom-expressions]
  S2 --> S3[plantilla clonable mas ligaduras]
  S3 --> S4[nodos del DOM reales de una vez]
end

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 escribes
const saludo = <div class="card">Hola, {nombre()}</div>;
// lo que genera, en esencia
import { template as _tmpl, insert as _insert } from "solid-js/web";

// 1) la porcion ESTATICA se hornea en una plantilla clonable
const _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;
})();
ℹ️
El compilador reparte estático y dinámico

La regla mental es simple: todo lo que el compilador puede probar que es constante entra en la cadena de template() y se hornea en HTML; todo lo que depende de un signal sale de ahí y se envuelve en insert() o effect(). Por eso una plantilla con mucho contenido fijo y pocos puntos reactivos genera código minúsculo: el navegador clona un nodo y Solid solo vigila los tres o cuatro sitios que de verdad cambian.

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.

🧬

Cero diffing

Nunca se compara un árbol con otro. Cuando un signal cambia, el efecto que lo observa actualiza su nodo directamente. El trabajo es proporcional a lo que cambió, no al tamaño de la vista.

🪶

Arranque ligero

Crear la UI es clonar plantillas y registrar unos efectos. No se materializa ningún grafo de objetos en memoria solo para describir el DOM que ya vas a crear.

🎯

Actualización quirúrgica

El texto {nombre()} es su propio nodo de texto con su propio efecto. Cambiar el nombre no toca ni reevalúa el <div> que lo contiene.

Eventos delegados

Los manejadores como onClick no se atan a cada nodo: Solid los delega en la raíz del documento y despacha por el árbol, ahorrando memoria y coste de montaje.

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.

El compilador es el framework: lo que ves no es lo que corre

La lección más liberadora sobre Solid llega cuando aceptas que el JSX que escribes y el código que se ejecuta son dos artefactos distintos, unidos por un compilador que hace casi todo el trabajo pesado antes de que el navegador vea una sola línea. En React, el framework es sobre todo tiempo de ejecución: una librería que interpreta tus elementos, mantiene un árbol virtual y decide en cada momento qué reconciliar. En Solid, la mayor parte del framework es tiempo de compilación: un transformador que lee tu intención declarativa y emite el código imperativo mínimo que la cumple —clonar esta plantilla, insertar aquí, envolver esta asignación en un efecto—. Esto explica de un plumazo casi todas las peculiaridades de Solid que confunden a quien viene de React. No destructuras props porque el compilador necesita ver los accesos para ligarlos; el componente corre una vez porque no hay ciclo de render que repetir; no hay key porque no hay reconciliación que orientar. Ninguna es una restricción arbitraria: son la sombra que proyecta el compilador sobre la sintaxis. Cuando algo te sorprenda, no preguntes qué hace el runtime; abre las herramientas de desarrollo, mira el módulo compilado y lee las llamadas a template, insert y effect. Ahí está la verdad del framework, escrita en un lenguaje más honesto que el JSX.

⚔️ Lee la salida del compilador
  1. En un proyecto Solid, escribe const v = <p class="x">Hola {nombre()}</p> y busca en las herramientas de red el módulo .tsx ya compilado que sirve Vite.
  2. Localiza la llamada a template() y comprueba qué parte de tu JSX quedó horneada como cadena estática y cuál salió como insert.
  3. Añade un atributo reactivo como title={t()} y verifica que aparece un effect() nuevo envolviendo esa asignación.
  4. Cambia un onClick por on:click y observa en el módulo compilado cómo desaparece la delegación y aparece un addEventListener directo.