Anatomía del output: template, clonar, insert y effect
La disección línea por línea del JavaScript que el compilador emite para un componente: la plantilla izada al módulo, el clon del nodo con `cloneNode`, la navegación estática por `firstChild` y `nextSibling`, el `insert()` que crea un efecto de render para el contenido vivo, y el `effect()` agrupado con el objeto de valores previos `_p$` que parchea varios atributos con una sola suscripción y guardas de igualdad. Cinco piezas que convierten el output compilado en un mapa exacto de dónde y por qué reacciona tu interfaz.
En la lección anterior viste la tubería; ahora vamos a diseccionar su producto. Tomaremos un componente corriente y leeremos, línea por línea, el JavaScript que el compilador emite: la plantilla izada al módulo, el clon del nodo, la navegación por sus hijos, el insert() que liga el contenido vivo y el effect() que parchea los atributos. Cuando reconozcas estas cinco piezas a simple vista, el output compilado deja de ser ruido y se convierte en un mapa exacto de dónde y por qué reacciona tu interfaz.
- Leer el output completo de un componente y nombrar cada una de sus cinco piezas.
- Entender por qué la plantilla se iza al módulo y se instancia con
cloneNode. - Ver cómo
insert()crea internamente un efecto de render para el contenido dinámico. - Descifrar el
effect()agrupado con el objeto de valores previos_p$que parchea atributos.
La plantilla se iza y se clona
Partimos de una tarjeta con dos zonas fijas —el contenedor y un título— y dos zonas vivas —una clase condicional y el texto de un span—.
function Tarjeta(props: { nombre: () => string; activo: () => boolean }) {
return (
<div class="tarjeta" classList={{ activa: props.activo() }}>
<h2>Perfil</h2>
<span>{props.nombre()}</span>
</div>
);
}
Lo primero que aparece en el output es una constante al nivel del módulo, fuera de la función. template() corre una sola vez al cargar el módulo: crea un elemento template del navegador, parsea el HTML y guarda su subárbol ya construido.
import { template as _$template } from "solid-js/web";
const _tmpl$ = _$template(`<div class=tarjeta><h2>Perfil</h2><span>`);
Todo el esqueleto estático —el div, su clase, el h2 con su texto y el span vacío— viaja en esa única cadena. Cada llamada posterior a _tmpl$() devuelve un cloneNode(true) profundo de ese subárbol ya parseado, que es la forma más rápida de fabricar DOM que existe. Una sola plantilla, compartida por todas las instancias de Tarjeta y por cada recreación. El compilador ya sabía, antes de que existiera el navegador, qué era constante y qué dependía del grafo; esa frontera decide todo lo que viene después.
Clonar, navegar, ligar
El cuerpo del componente compila a una IIFE que clona el esqueleto, camina hasta los nodos vivos por punteros fijos y engancha ahí la reactividad.
import { insert as _$insert, effect as _$effect, classList as _$classList } from "solid-js/web";
const Tarjeta = (props) => {
return (() => {
const _el$ = _tmpl$(); // clona el esqueleto
const _el$2 = _el$.firstChild; // el <h2>
const _el$3 = _el$2.nextSibling; // el <span>
_$insert(_el$3, () => props.nombre()); // contenido vivo del span
_$effect(() => _$classList(_el$, { activa: props.activo() }));
return _el$;
})();
};
La navegación por firstChild y nextSibling no es casual: el compilador calcula en tiempo de build una ruta estática de punteros hasta cada nodo dinámico, sin querySelector, sin id, sin búsquedas. Son saltos de puntero, deterministas y baratos, resueltos antes de que el código corra. Si añadieras un nodo estático entre el h2 y el span, el compilador recalcularía la ruta —un nextSibling más— pero el coste en tiempo de ejecución seguiría siendo el mismo: caminar el esqueleto una vez al crear la instancia.
Cuando el contenido dinámico cae entre dos zonas fijas, el compilador no puede insertarlo al final: crea un nodo marcador y se lo pasa a insert como tercer argumento para fijar la posición exacta.
<p>Hola {nombre()}, tienes {n()} mensajes</p>
// dos huecos vivos entre texto fijo, cada insert con su referencia
const _el$2 = _el$.firstChild.nextSibling; // nodo marcador tras "Hola "
_$insert(_el$, nombre, _el$2); // inserta ANTES de _el$2, no al final
_$insert(_el$, n, null); // este si va al final
El marcador es la respuesta del compilador a un problema puramente posicional: mantener cada trozo dinámico en su sitio aunque el que tiene delante crezca o encoja.
insert y effect: el contenido y los atributos
insert(padre, accessor, marcador) es la pieza que da vida al contenido. Cuando el segundo argumento es una función, insert la envuelve internamente en un createRenderEffect: cualquier signal que se lea dentro re-ejecuta solo esa inserción. Actualiza el nodo de texto en el sitio si el valor sigue siendo texto, o reemplaza nodos si cambia su forma. El tercer argumento —el marcador, aquí ausente— indica ante qué hermano insertar. Esta es la actualización quirúrgica: cambiar nombre despierta solo este efecto y toca solo el nodo de texto de este span, sin rozar el div que lo contiene.
Ese envoltorio interno es literal. insert mira el tipo de lo que recibe y, si es una función, la mete en un efecto de render propio; si es un valor plano, lo escribe una vez y termina. Ahí está, en una condición, la diferencia entre reaccionar y no reaccionar.
// esbozo de lo que insert hace por dentro
function insert(padre, valor, marcador) {
if (typeof valor === "function")
createRenderEffect(() => reconciliar(padre, valor(), marcador));
else reconciliar(padre, valor, marcador); // dato plano: una sola vez
}
El otro extremo son los atributos. Cuando un elemento tiene varios reactivos, el compilador no crea un efecto por cada uno: los agrupa en un solo efecto con un objeto de valores previos que evita escrituras inútiles.
<a href={url()} class={clase()} title={titulo()}>ir</a>
_$effect((_p$) => {
const _v$ = url(), _v$2 = clase(), _v$3 = titulo();
_v$ !== _p$.e && _$setAttribute(_el$, "href", _p$.e = _v$);
_v$2 !== _p$.t && _$className(_el$, _p$.t = _v$2);
_v$3 !== _p$.a && _$setAttribute(_el$, "title", _p$.a = _v$3);
return _p$;
}, { e: undefined, t: undefined, a: undefined });
Un único efecto de render lee los tres accessors, de modo que se re-ejecuta si cualquiera cambia; pero el objeto _p$ cachea el último valor de cada uno y las guardas !== garantizan que la escritura al DOM ocurra solo para el atributo que de verdad cambió. Una suscripción, tres escrituras vigiladas. Por eso un elemento con varios atributos vivos no genera varios efectos: genera uno, barato de suscribir y preciso al aplicar.
Fíjate además en que cada tipo de atributo tiene su propio helper. El compilador ya sabe, en build, cuál corresponde a cada binding, así que en runtime no queda ninguna decisión que tomar: className toca la propiedad class de un paso, style fusiona el objeto de estilos y setProperty escribe en la propiedad del nodo, no en el atributo.
_$className(_el$, clase()); // ruta directa a class
_$style(_el$, estilos()); // fusiona el objeto de estilos
_$setProperty(_el$, "value", v()); // propiedad, no atributo
Izar
La plantilla se declara una vez al nivel del módulo con template(), fuera de la función que la usa.
Clonar
Cada instancia es un cloneNode(true) del subárbol ya parseado: creación de DOM al mínimo coste.
Navegar
Rutas fijas de firstChild y nextSibling, calculadas en build, alcanzan cada nodo vivo.
Insertar y parchear
insert() liga el contenido con un efecto de render; effect() agrupa los atributos con guardas _p$.
Leer el JavaScript compilado de un componente es como leer una radiografía: cada estructura que aparece contesta una pregunta precisa que el JSX dejaba implícita. La constante template al nivel del módulo responde a “qué parte de esto es fija”, y su respuesta —una cadena de HTML— es también una promesa de rendimiento, porque significa que ese fragmento se parsea una vez y se clona sin coste el resto de la vida del programa. Las líneas de navegación con firstChild y nextSibling responden a “cómo llego a lo que cambia”, y revelan que Solid no busca nodos en tiempo de ejecución sino que memoriza rutas en tiempo de compilación. El insert responde a “dónde vive el contenido dinámico” y, al recibir una función, delata que por debajo se ha creado un efecto de render suscrito a los signals que leas. Y el effect agrupado con su objeto _p$ responde a la pregunta más sutil —“cómo se actualizan varios atributos sin desperdiciar trabajo”— con una elegancia que merece estudiarse: una sola suscripción que despierta ante cualquiera de sus dependencias, pero un juego de guardas de igualdad que reduce las escrituras al DOM exactamente a las que cambiaron. Cuando internalizas que estas cinco piezas —izar, clonar, navegar, insertar, parchear— son el vocabulario completo del output, dejas de leerlo como un jeroglífico y empiezas a leerlo como prosa. Y esa fluidez es la que te permite, ante cualquier duda sobre por qué algo reacciona o se queda quieto, ir directo a la línea que lo explica.
- Compila el componente
Tarjetade esta lección en el Playground de Solid y confirma que elh2con su texto viajó dentro de la cadena detemplate(). - Localiza las líneas de navegación y dibuja la ruta de punteros desde
_el$hasta elspan. - Añade un tercer atributo reactivo al
divy verifica que aparece dentro del mismo efecto, con su propia entrada en el objeto_p$. - Convierte el texto del
spanen algo estático comoPerfily comprueba que elinsertdesaparece porque el contenido pasó a la plantilla. - Explica, señalando líneas del output, por qué cambiar
nombreno re-ejecuta el efecto declassList.