Saltar al contenido

Por qué Solid es rápido: leer el js-framework-benchmark

El js-framework-benchmark es lo más parecido a una regla objetiva que tiene el frontend, y Solid vive pegado a la cima. Qué mide de verdad su batería de operaciones sobre una tabla, las tres familias de métricas —duración de actualización, arranque y memoria—, por qué la media geométrica ponderada frente a vanilla resiste los valores atípicos, la mecánica de grano fino que explica los números, y cómo leerlos con honestidad: qué certifica el benchmark y qué deja fuera del todo.

Qué mide, operación por operación

El benchmark no evalúa apps: evalúa una única tabla sometida a una coreografía fija de manipulaciones del DOM. Cada implementación —de vanillajs a React— repite las mismas nueve operaciones de duración, cronometradas decenas de veces y con la mediana como veredicto. Las operaciones son deliberadamente elementales: crear 1.000 filas, reemplazar las 1.000, actualizar el texto de cada décima fila sobre una tabla de 10.000, seleccionar una fila al hacer clic, intercambiar dos filas, eliminar una, crear 10.000 de golpe, añadir 1.000 a una tabla ya poblada y vaciarla entera.

Cada una aísla un coste distinto del renderizado. Crear mide la fabricación de nodos; reemplazar y vaciar miden la reconciliación de listas enteras; la actualización parcial mide la precisión quirúrgica —tocar 1.000 celdas de 10.000 sin rozar las demás—; intercambiar mide el movimiento de nodos preservando identidad. Es un microscopio sobre la parte del trabajo que un framework de UI no puede delegar: traducir un cambio de datos en un cambio de DOM.

La metodología cuida los detalles que invalidarían un benchmark descuidado. Cada operación se cronometra decenas de veces sobre un Chrome controlado por automatización, con una fase de calentamiento previa para que el JIT esté caliente, se descartan los valores atípicos y se reporta la mediana en lugar de la media para que un pico aislado del recolector de basura no distorsione el número. El tiempo medido no es el del click sino el que va desde la acción hasta que el navegador termina de pintar, capturado con las trazas de rendimiento del propio Chrome. Es reproducible, versionado y abierto: cualquiera puede clonar el repositorio y correrlo.

El mismo componente de fila se escribe con For en la variante keyed y con Index en la non-keyed; esa única elección decide qué contrato estás midiendo y por qué no se pueden comparar entre sí.

TSX
// keyed: identidad por referencia; reordenar mueve nodos (For)<For each={filas()}>{(fila) => <Fila dato={fila} />}</For>// non-keyed: reutiliza el nodo por posicion; el dato es un signal (Index)<Index each={filas()}>{(fila) => <Fila dato={fila()} />}</Index>

Las tres familias: actualización, arranque, memoria

Las nueve operaciones anteriores forman solo la primera familia: duración, el coste en milisegundos de mutar el DOM. Pero un framework rápido en actualizar puede ser lento en arrancar o glotón en memoria, así que el benchmark mide otras dos familias que a menudo se ignoran al citar “el número de Solid”.

La segunda es el arranque: el peso total transferido en kilobytes, el tiempo que el navegador dedica a evaluar el script (script bootup), el trabajo total en el hilo principal y una métrica pesimista de interactividad (consistently interactive). Aquí no cuenta lo rápido que actualizas, sino cuánto JavaScript obligas a descargar, parsear y ejecutar antes de que la página responda. La tercera es la memoria: cuánta se retiene tras cargar, tras crear 1.000 filas, tras repetir actualizaciones, y —la prueba más reveladora— tras crear y vaciar 1.000 filas, que detecta fugas comparando el antes y el después.

Los tres ejes están en tensión. Un runtime que precalcula y cachea todo gana en actualización pero paga en memoria; uno que difiere trabajo gana en arranque pero puede tartamudear luego. Citar solo la duración es contar un tercio de la historia.

El eje de arranque merece un énfasis especial porque su coste no se reparte por igual. En un portátil de gama alta, medio megabyte de JavaScript se descarga y parsea en un parpadeo; en un teléfono de gama media con CPU modesta y red irregular, ese mismo medio megabyte puede costar segundos de parseo y ejecución antes de que la página responda a un toque. El peso del bundle no es un número abstracto: es tiempo de CPU multiplicado por lo lento que sea el dispositivo del usuario. Por eso el runtime de 7 kB de Solid no es una cifra de vanidad, sino una ventaja que se agranda justo en los dispositivos donde más duele, y por eso citar solo la duración —que se mide en una máquina potente— oculta parte de lo que un usuario real experimenta.

Por qué Solid queda pegado a vanilla

Los resultados se normalizan contra vanillajs, la implementación a mano que recibe el factor 1.00. Cada framework obtiene, por operación, un factor igual a su tiempo dividido por el del más rápido, y su nota final es la media geométrica de esos factores. La elección de media geométrica y no aritmética es deliberada: impide que una sola operación catastrófica —o una sola brillante— domine el total, porque multiplica factores en vez de sumarlos. Un framework que va 1.1x en ocho pruebas y 5x en una no puede esconderlo, pero tampoco queda sepultado por ese único tropiezo.

TypeScript
// La nota es la media geometrica de los factores, no la aritmetica:// la raiz enesima del producto castiga los picos sin dejar que dominen.const factores = [1.05, 1.1, 1.08, 1.2, 1.03]; // uno por operacionconst nota = Math.pow(factores.reduce((a, b) => a * b, 1), 1 / factores.length);

Solid ronda un factor de media cercano a 1.05-1.12 sobre vanilla en la variante keyed: entre los tres primeros de toda la tabla, y por delante de cada framework con VDOM por un margen estructural, no anecdótico. La razón es la misma en los tres ejes. En actualización, la reactividad de grano fino conecta cada signal directamente con el nodo del DOM que depende de él, así que una actualización parcial toca exactamente esas celdas sin comparar árboles ni re-ejecutar componentes. En arranque, el runtime pesa alrededor de 7 kB porque no transporta un reconciliador; el compilador convierte el JSX en plantillas que se clonan con cloneNode y en expresiones reactivas mínimas. En memoria, no hay árbol virtual ni fibras que retener: solo el grafo reactivo, más ligero que cualquier representación paralela del DOM.

El fragmento que el benchmark ejecuta miles de veces es, en esencia, este: una lista keyed que reconcilia por referencia y actualiza celdas por su signal.

TSX
import { For } from "solid-js";// keyed: cada fila sigue a su dato; reordenar mueve nodos, no los recrea<tbody>  <For each={filas()}>    {(fila) => (      <tr classList={{ danger: seleccionada() === fila.id }}>        <td>{fila.id}</td>        <td><a onClick={() => seleccionar(fila.id)}>{fila.etiqueta}</a></td>      </tr>    )}  </For></tbody>

Lo que el compilador emite a partir de ese JSX es la clave de los números de arranque y actualización. No genera funciones de render que se re-ejecutan: genera una plantilla que se clona con cloneNode y una lista de expresiones reactivas, una por cada hueco dinámico, que se atan al nodo exacto. Conceptualmente, la fila de arriba se convierte en algo cercano a esto: un molde estático que se clona por instancia, y efectos mínimos que actualizan solo su celda.

TSX
// Conceptual: lo que el compilador produce por filaconst molde = template(`<tr><td></td><td><a></a></td></tr>`);const fila = molde.cloneNode(true);        // DOM real, sin VDOMconst celdaId = fila.firstChild;effect(() => (celdaId.textContent = fila.id));  // toca solo esta celda

Clonar un molde es de las operaciones más baratas del navegador, y atar un efecto por hueco significa que una actualización parcial ejecuta exactamente tantos efectos como celdas cambian. Ahí están, a la vez, el arranque diminuto —no se envía un reconciliador— y la actualización quirúrgica que roza a vanillajs.

Hay un detalle más que explica la operación de seleccionar fila del benchmark: la delegación de eventos. En lugar de registrar un manejador por fila —diez mil manejadores en la tabla grande—, Solid registra uno solo en la raíz y deja que el evento burbujee, resolviendo en tiempo de despacho qué fila lo originó. Eso abarata tanto la creación —no se instalan miles de listeners— como la memoria —no se retienen—, y es la clase de optimización que en vanillajs escribirías a mano y que en Solid viene de serie por cómo el compilador trata los eventos del JSX.

Leer los números con honestidad

Un profesional que cita este benchmark debe saber qué no mide, porque su fama invita a sobreinterpretarlo. Mide una carga sintética dominada por manipulación bruta del DOM y reconciliación de listas. No mide la obtención de datos, ni el coste de la lógica de negocio, ni la ergonomía del desarrollo, ni la arquitectura de una app real con rutas, formularios y estado compartido. En una aplicación donde el cuello de botella es la red o un cálculo pesado, la diferencia entre el puesto uno y el puesto cinco puede ser ruido frente al tiempo que se va en un fetch.

Ser el más rápido aquí certifica una cosa concreta y valiosa: que el techo de rendimiento de la capa de renderizado es altísimo y que no pagas un impuesto de runtime por cada actualización. Es la garantía de que, cuando tu interfaz sí sea el cuello de botella —una tabla viva, un editor, un dashboard que se refresca sin descanso—, el framework no será quien te frene. Pero no es una promesa de que toda app en Solid sea rápida: un mal patrón de datos o un cálculo síncrono caro te hunden en cualquier framework. El benchmark mide el suelo del renderizado, no el destino de tu aplicación.

La forma profesional de usar esta tabla, entonces, no es como un decisor único sino como un filtro y una cota. Como filtro, descarta la preocupación de que la capa de renderizado sea tu limitación: si eliges un framework del pelotón de cabeza, sabes que el techo está por encima de lo que tu app necesitará casi siempre. Como cota, te dice cuánto margen tienes antes de que el propio framework aparezca en un perfil de rendimiento. La decisión real la tomas luego con el profiler sobre tu app concreta, tu red concreta y tus dispositivos concretos, que es donde viven los cuellos de botella que este benchmark, por diseño, no puede ver. Respetar esa división del trabajo —el benchmark para el techo, el profiler para tu caso— es lo que separa a quien cita números de quien los entiende.

Y hay un corolario que Solid disfruta y pocos comparten: como su techo está tan alto y su código idiomático tan cerca de ese techo, el margen entre “lo que escribes sin pensar” y “lo óptimo” es estrecho. En la práctica, eso significa menos horas de perfilado y menos reescrituras defensivas, porque el suelo del que partes ya es casi el techo. El benchmark no te promete una app rápida, pero sí te promete que empezarás muy cerca de lo mejor posible, y en ingeniería de rendimiento el punto de partida importa tanto como el destino.