Saltar al contenido

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.

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.

JS
// izada y compartida por cada fila del Forconst _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$.

TSX
const cabecera = <li class="fila"><span /></li>;const pie      = <li class="fila"><span /></li>;
JS
// una sola plantilla para ambasconst _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.

TSX
<article class="post">  <h1>Solid</h1>  <p class="cuerpo">{contenido()}</p></article>
JS
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.

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.

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.

JS
_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:

TSX
// 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.

JS
// 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.