Optimizaciones del compilador: izado, clonado y separación
Las técnicas con las que el compilador exprime la partición estático/dinámico. El izado de plantillas al ámbito del módulo y su deduplicación, el clonado con `cloneNode` como la vía más rápida de crear DOM, y el aislamiento de las islas dinámicas para que solo ellas paguen efectos. Más un catálogo de micro-optimizaciones que se acumulan: soltar comillas, agrupar atributos en un efecto con `_p$`, envolver condicionales en memos con `wrapConditionals`, delegar eventos y el marcador `@once` para salir del tracking.
Ya sabes que el compilador parte cada plantilla en una porción estática clonable y unas ligaduras reactivas. Esta lección abre el capó de esa partición y muestra las optimizaciones concretas que la hacen rendir: el izado de plantillas al módulo, su clonado nativo, el aislamiento de lo dinámico y una constelación de micro-optimizaciones que, sumadas, explican por qué el código que Solid envía es tan pequeño y tan directo. Entender estas técnicas es entender por qué una lista de diez mil filas comparte una sola plantilla y vigila solo lo que de verdad cambia.
- Comprender el izado estático de plantillas al ámbito del módulo y su deduplicación.
- Justificar por qué
cloneNodees la creación de DOM más barata y cómo la explota el compilador. - Ver cómo el aislamiento estático/dinámico hace que solo las islas vivas paguen efectos.
- Catalogar las micro-optimizaciones —comillas,
_p$,wrapConditionals, delegación,@once— y cuándo actúan.
Izado estático: la plantilla vive una vez
El compilador saca cada plantilla del cuerpo del componente y la declara al nivel del módulo, como una constante _tmpl$. Se crea una sola vez al cargar el módulo y la comparten todas las instancias y todas las recreaciones de ese JSX. Además, si dos fragmentos de JSX producen exactamente el mismo HTML, el compilador los deduplica en una única _tmpl$. La consecuencia es contundente: en un <For> de diez mil filas no hay diez mil plantillas, hay una, y cada fila es un clon de ella.
// izada y compartida por cada fila del For
const _tmpl$ = _$template(`<li class=fila><span></span></li>`);
Que la plantilla viva fuera de la función tiene un segundo efecto sutil: no se reconstruye aunque el componente se cree y destruya mil veces. El coste de parsear ese HTML se paga una vez en la vida del módulo, no una vez por instancia. Es la diferencia entre un molde y las piezas que salen de él.
Y si el módulo repite el mismo marcado en dos sitios, no habrá dos constantes: la deduplicación las funde en una. Estas dos vistas comparten una única _tmpl$.
const cabecera = <li class="fila"><span /></li>;
const pie = <li class="fila"><span /></li>;
// una sola plantilla para ambas
const _tmpl$ = _$template(`<li class=fila><span></span></li>`);
const cabecera = _tmpl$(), pie = _tmpl$();
Clonado: la creación de DOM más barata
Instanciar una plantilla es llamar a cloneNode(true) sobre un subárbol ya parseado. Esta es la ruta de construcción de DOM más rápida que ofrece el navegador: el HTML se analizó una sola vez, al crear el template, y a partir de ahí clonar copia ese subárbol en código nativo, mucho más veloz que una secuencia de createElement, setAttribute y appendChild interpretada en JavaScript. El compilador convierte “construye este subárbol” en “clona este subárbol”, y esa traducción es gran parte de la ventaja de arranque de Solid.
La diferencia se agranda con el tamaño del fragmento. Un componente con veinte nodos estáticos y un punto dinámico se crea con un solo cloneNode que trae los veinte de golpe, más un insert para el punto vivo. El enfoque imperativo equivalente serían veinte llamadas de creación y montaje; el enfoque de VDOM, veinte objetos que describir y luego materializar. Clonar los salta todos.
Míralo en un componente con mucho fijo y un punto vivo: casi todo se hornea en la cadena y solo el párrafo queda vigilado.
<article class="post">
<h1>Solid</h1>
<p class="cuerpo">{contenido()}</p>
</article>
const _tmpl$ = _$template(`<article class=post><h1>Solid</h1><p class=cuerpo>`);
const _el$ = _tmpl$(), _el$2 = _el$.firstChild.nextSibling;
_$insert(_el$2, contenido); // el unico effect del componente
Separar lo estático de lo dinámico
Solo las islas dinámicas reciben insert o effect; todo lo demostrablemente constante se hornea en la cadena. El análisis isDynamic es quien traza la frontera: atributos fijos, textos fijos y subárboles enteros sin ninguna expresión reactiva caen del lado estático y desaparecen dentro del HTML. Por eso una plantilla con mucho contenido fijo y pocos puntos vivos genera código minúsculo: se clona un nodo y se vigilan tres sitios.
flowchart LR J[arbol de JSX] --> A[analisis isDynamic] A -->|constante| T[cadena de template izada y deduplicada] A -->|reactivo| B[insert y effect solo aqui] T --> C[cloneNode por instancia] B --> C C --> D[DOM real con islas vigiladas]
La lección de diseño es que el rendimiento no viene de optimizar lo dinámico sino de maximizar lo estático. Cada trozo que el compilador logra probar constante es un efecto que no se crea, una suscripción que no existe y unos bytes que se funden en una cadena. Escribir componentes con zonas fijas amplias y puntos reactivos concretos no es solo buen estilo: es alimentar al compilador con la partición que mejor sabe explotar.
El corolario práctico es que un antipatrón frecuente —envolver texto fijo en una expresión— le roba trabajo al compilador. Escribir {"Perfil"} en vez de Perfil fuerza un insert donde bastaba una cadena horneada; interpolar una constante que podrías haber escrito literal convierte estático en dinámico sin ninguna necesidad. La regla es dejar fija toda la prosa que no dependa de un signal y reservar las llaves para lo que de verdad cambia.
Conviene además saber que este izado convierte a la plantilla en un valor de primera clase: se referencia, se cuenta y se comparte. La deduplicación es por módulo, así que dos componentes en ficheros distintos que casualmente producen el mismo HTML no comparten _tmpl$; pero dentro de un mismo módulo el compilador es implacable reutilizando, y ese ámbito coincide casi siempre con el de un componente y sus fragmentos.
En conjunto, las tres técnicas grandes —izar, clonar, separar— no compiten sino que se refuerzan: se iza para poder compartir, se comparte para poder clonar barato, y se clona porque lo estático ya quedó aislado de lo dinámico. Quitar cualquiera de las tres desharía a las otras dos, y por eso conviene verlas como un solo mecanismo con tres caras y no como optimizaciones sueltas.
Micro-optimizaciones que se acumulan
Sobre esas tres ideas grandes, el compilador aplica un rosario de afinamientos que, sumados, recortan bytes y trabajo.
Soltar comillas
En la cadena de la plantilla, class="fila" se emite como class=fila cuando el valor no las necesita: menos bytes en cada template.
Agrupar atributos
Varios atributos reactivos de un elemento comparten un effect con el objeto _p$ de valores previos y guardas de igualdad.
wrapConditionals
Los && y los ternarios con partes reactivas se envuelven en un memo, así la rama solo se recompone cuando la condición cambia de verdad.
Delegar eventos
onClick no hace addEventListener por nodo: guarda el handler como propiedad y registra un único listener global con delegateEvents.
La delegación de eventos merece un vistazo al output. Un onClick no ata nada al botón: guarda tu handler como una propiedad especial del nodo y registra el tipo de evento una sola vez en el documento; un único listener global despacha todos los clics siguiendo la jerarquía. Si necesitas un listener nativo directo, la forma on:click compila a un addEventListener literal.
El ahorro es real en listas grandes: mil botones con onClick no registran mil listeners, solo guardan mil propiedades y comparten un único punto de escucha en el documento. Montar y desmontar filas se abarata porque no hay addEventListener ni removeEventListener que ejecutar por nodo. Es una de esas optimizaciones que no cambian el resultado visible pero recortan memoria y trabajo de montaje en cada render de una colección.
_el$.$$click = manejar; // handler como propiedad, no addEventListener
_$delegateEvents(["click"]); // un unico listener global, registrado una vez
El marcador @once merece mención aparte porque es la única de estas optimizaciones que tú invocas a mano. Cuando sabes que una expresión no cambiará, un comentario /*@once*/ justo antes le dice al compilador que la lea una vez y no genere efecto:
// se lee al crear y nunca mas: no hay effect para title
<a title={/*@once*/ etiquetaInicial()} href={url()}>ir</a>
El href sigue siendo reactivo, pero title se congela deliberadamente. Es la vía para bajar del tracking cuando la reactividad sobra, el reverso exacto de la partición: aquí eres tú quien empuja algo del lado dinámico al estático.
Y así se ve wrapConditionals en el output: la condición envuelta en un memo que solo notifica cuando su booleano cambia.
// condicion() ? <A/> : <B/>
const _c$ = _$memo(() => !!condicion());
_$insert(_el$, () => _c$() ? _$createComponent(A, {}) : _$createComponent(B, {}));
Sin ese memo, cualquier señal leída dentro de las ramas re-evaluaría el ternario entero; con él, cambiar algo interno de A no reconstruye la rama mientras la condición siga siendo verdadera. Es una optimización que no pediste y que el compilador coloca por ti.
Sin wrapConditionals, un condicion() ? <A /> : <B /> dentro de un insert re-evaluaría la rama en cada tick de cualquier dependencia interna. Con la opción activa —lo está por defecto en babel-preset-solid— el compilador envuelve la condición en un memo, de modo que la rama solo se recrea cuando el valor booleano de la condición cambia, no cada vez que algo dentro de la rama se mueve. Es la razón de que puedas escribir ternarios y && en el JSX sin miedo, aunque para ramas con estructura sigas prefiriendo Show por claridad.
Si tuvieras que quedarte con una idea de este nivel entero, que sea esta: casi todo lo que hace listo al compilador de Solid se deduce de una única decisión —dónde trazar la frontera entre lo estático y lo dinámico— aplicada con disciplina hasta sus últimas consecuencias. El izado de plantillas es esa frontera vista desde el lado estático: si algo es constante para todas las instancias, no tiene por qué vivir dentro de la función, así que sube al módulo y se comparte, y si dos fragmentos coinciden se funden en uno. El clonado es la misma frontera vista como oportunidad: lo estático, al estar ya parseado, se reproduce con la operación más barata del navegador en lugar de reconstruirse instrucción a instrucción. La deduplicación, el soltar comillas, el hornear subárboles enteros son todos el lado estático llevado al límite: exprimir cada byte y cada milisegundo de lo que probamos que no cambia. Y en el lado dinámico, la misma frontera manda: agrupar atributos en un efecto con guardas _p$, envolver condicionales en memos, delegar eventos, ofrecerte @once para reclasificar a mano lo que sabes fijo, son maneras de que lo dinámico pague lo justo y ni un efecto de más. No hay una bolsa de trucos inconexos que memorizar; hay un principio —separar lo que cambia de lo que no— y una voluntad de aplicarlo sin excepción. Cuando veas una optimización nueva en el output, no la archives como curiosidad: pregúntate de qué lado de la frontera vive y entenderás al instante por qué el compilador la hizo.
- Renderiza el mismo
<li>dos veces en el módulo y confirma en el output que ambas comparten una única_tmpl$deduplicada. - Pon tres atributos reactivos en un elemento y verifica que se agrupan en un solo
effectcon su objeto_p$. - Añade un
condicion() ? <A /> : <B />y localiza elmemoque introducewrapConditionalsalrededor de la condición. - Compara el output de
onClickfrente aon:clicky observa cómo uno delega y el otro haceaddEventListenerdirecto. - Marca un atributo con
/*@once*/y comprueba que sueffectdesaparece mientras los vecinos siguen reactivos.