Optimización práctica: For vs Index, memoizar, dividir el bundle
El techo de rendimiento del capítulo anterior no se hereda gratis: se conquista con decisiones concretas. Elegir For o Index según si la lista se reordena o solo cambia de valor, memoizar únicamente lo caro o lo compartido y no lo barato, evitar las recreaciones de subárboles que nacen del control flow, y dividir el bundle con lazy y Suspense para que el arranque siga siendo diminuto. El catálogo de decisiones que separan una app Solid rápida de una que desperdicia su arquitectura.
Que Solid roce a vanillajs en el benchmark no significa que tu app lo hará: el techo es altísimo, pero se pierde con facilidad. La reactividad de grano fino elimina el problema del re-render, pero deja intactas otras decisiones que sí controlas —qué helper de listas usas, qué derivaciones memoizas, cuándo el control flow reconstruye un subárbol entero, cuánto JavaScript envías de golpe—. Esta lección es el catálogo operativo de esas decisiones. No son micro-optimizaciones esotéricas: son las cuatro palancas que, bien accionadas, mantienen tu app pegada al techo de su arquitectura, y mal accionadas la alejan sin que el grafo pueda salvarte.
- Elegir entre
ForeIndexsegún reordenación por referencia o cambio de valor por posición. - Memoizar solo lo caro o lo compartido, y reconocer cuándo un
createMemosobra. - Evitar recreaciones de subárboles usando
Showy control flow en vez de ternarios crudos. - Dividir el bundle con
lazyySuspensepara preservar el arranque diminuto.
For o Index: identidad por referencia frente a posición
La decisión más frecuente y peor entendida es cuál de los dos helpers de listas usar. For es keyed por referencia: asocia cada nodo del DOM al objeto que lo generó, así que cuando la lista se reordena o se filtra, Solid mueve los nodos existentes en lugar de recrearlos. Dentro del callback, el ítem es un valor plano y el índice es un signal. Index es lo contrario: keyed por posición. El nodo de la posición cero siempre es el mismo nodo, y lo que cambia es su contenido; el ítem es un signal y el índice es un número fijo.
La regla operativa se deduce de qué es estable. Si tu lista se reordena, se filtra o se inserta en medio, y sus elementos son objetos con identidad —usuarios, tareas, mensajes—, usa For: preservar el nodo por referencia mantiene el estado del DOM (foco, scroll, animaciones) atado al dato correcto cuando este se mueve. Si tu lista tiene longitud estable, contiene primitivos, o quieres que el nodo de una posición persista mientras solo cambia su valor —un conjunto de inputs editables, un ecualizador, filas cuyo orden no cambia—, usa Index: reutilizar el nodo por posición evita recrear DOM cuando lo único que muta es el texto.
import { For, Index } from "solid-js";
// For: se reordena y los items tienen identidad -> mueve nodos por referencia
<For each={tareas()}>{(tarea) => <Tarea dato={tarea} />}</For>
// Index: longitud estable, editas el valor en su sitio -> el input no se recrea
<Index each={campos()}>
{(valor, i) => <input value={valor()} onInput={(e) => setCampo(i, e.currentTarget.value)} />}
</Index>
Elegir mal duele en las dos direcciones. Index sobre una lista reordenable recrea contenido en cada movimiento y pierde el foco del input que el usuario estaba editando; For sobre una lista de primitivos que solo cambian de valor recrea el nodo entero cuando bastaba con actualizar el texto. El helper correcto no es cuestión de gusto: es cuestión de qué preservas.
Hay una simetría reveladora en sus firmas que ayuda a recordar cuál es cuál: en For el ítem es un valor y el índice es un signal, porque lo que cambia es la posición; en Index el ítem es un signal y el índice es un número fijo, porque lo que cambia es el valor. La firma te dice qué considera estable cada helper, y elegir bien es alinear esa estabilidad con la de tus datos.
flowchart TD
Q[la lista cambia como] --> R{se reordena o filtra}
R -->|si con items objeto| F[usa For keyed por referencia]
R -->|no solo cambia el valor| I[usa Index keyed por posicion]
F --> FB[preserva foco scroll y animacion del dato]
I --> IB[reutiliza el nodo no recrea el input]
style F fill:#89b4fa,color:#11111b
style I fill:#a6e3a1,color:#11111bMemoizar lo caro, no lo barato
createMemo cachea una derivación y la recalcula solo cuando cambian sus fuentes; además deduplica hacia abajo, porque no notifica a sus consumidores si el resultado nuevo es igual al viejo. Es tentador envolver toda derivación en un memo “por si acaso”, pero eso es un error: un memo cuesta un nodo en el grafo más el almacenamiento de su valor, y para un cálculo trivial ese overhead supera al ahorro. Una derivación barata leída en un solo sitio se escribe mejor como una función normal.
// Sobra el memo: la suma es barata y se lee una vez -> una funcion basta
const total = () => a() + b();
// Justifica el memo: calculo caro Y compartido por varios consumidores
const ordenadas = createMemo(() => items().slice().sort(comparadorCostoso));
Un createMemo se paga en tres situaciones y solo en ellas. Cuando el cálculo es caro —un sort, un filter sobre miles de elementos, un parseo— y se recalcularía de más sin la caché. Cuando el resultado es compartido por muchos consumidores, porque el memo lo computa una vez en lugar de una vez por lector. Y cuando quieres cortar la propagación con un equals a medida, para que un cambio en la fuente que produce un resultado estructuralmente igual no despierte a media app. Fuera de esos tres casos, memoizar es añadir un nodo que no gana nada.
El reflejo de memoizar todo viene de React, donde cada render recrea todo y useMemo mitiga ese coste. En Solid ese coste no existe: el cuerpo corre una vez y una derivación no se re-ejecuta salvo que su fuente cambie. Envolver derivaciones baratas en memos no acelera nada y sí ensucia el grafo con nodos superfluos que hay que crear, retener y disponer. Mide antes de memoizar: si no puedes nombrar cuál de los tres motivos —caro, compartido, cortar propagación— justifica el memo, no lo pongas.
Evitar recreaciones de subárboles
Con el re-render fuera de escena, la fuente principal de trabajo desperdiciado en Solid es el control flow que reconstruye subárboles. Un ternario crudo en el JSX que alterna entre dos ramas recrea nodos en cada cambio de la condición, porque Solid no puede saber que quieres cachearlos. Show existe justo para eso: crea la rama solo cuando la condición lo pide y la conserva, sin recomputarla mientras la condición no cambie de veracidad.
// Crudo: al alternar la condicion se recrean nodos en cada cambio
<div>{estaLogueado() ? <Panel /> : <Login />}</div>
// Idiomatico: Show gestiona la rama y no reconstruye de mas
<Show when={estaLogueado()} fallback={<Login />}>
<Panel />
</Show>
La misma disciplina se aplica a no recrear fuentes reactivas por accidente. Crear un createSignal o un createStore dentro de un bucle de renderizado, o dentro de un efecto que se re-ejecuta, fabrica una fuente nueva en cada vuelta y desperdicia la anterior. Y el error clásico que rompe el grano fino sin recrear nada: destructurar props, que congela el valor en el momento del montaje y desconecta el nodo del grafo.
// Roto: destructurar lee la prop una vez y pierde la reactividad
function Saludo({ nombre }: { nombre: string }) {
return <h1>Hola {nombre}</h1>; // nombre nunca se actualiza
}
// Correcto: accede a props.nombre en el JSX, sigue vivo en el grafo
function Saludo(props: { nombre: string }) {
return <h1>Hola {props.nombre}</h1>; // se actualiza al cambiar la prop
}
La recreación no siempre es visible como parpadeo; a veces es un signal huérfano, un efecto que se acumula o una prop que dejó de reaccionar. La heurística es preguntar, ante cada estructura que se monta y desmonta, si el control flow la está reconstruyendo cuando podrías conservarla, y ante cada prop, si la estás leyendo dentro del grafo o la congelaste al destructurar.
Dividir el bundle para proteger el arranque
La cuarta palanca no toca la actualización sino el arranque, la segunda familia de métricas del capítulo anterior. Un runtime de 7 kB no sirve de nada si encima cargas 400 kB de tu propio código y de librerías pesadas antes del primer pintado. lazy difiere la carga de un componente hasta que se necesita, y Suspense cubre la espera con un fallback; la combinación divide el bundle en trozos que llegan bajo demanda.
import { lazy, Suspense } from "solid-js";
// El editor pesado no entra en el bundle inicial: se carga al mostrarlo
const Editor = lazy(() => import("./Editor"));
<Suspense fallback={<Esqueleto />}>
<Show when={editando()}>
<Editor />
</Show>
</Suspense>
En una app con Solid Router, el troceado por rutas es casi automático: cada ruta perezosa es un punto de corte natural, de modo que el visitante de la página de inicio no descarga el código del panel de administración. Para librerías voluminosas —un editor de código, una librería de gráficos, un exportador de PDF— el import() dinámico las mantiene fuera del camino crítico y las trae solo cuando la interacción las reclama. Dividir el bundle es la palanca que conecta directamente con el eje de arranque: menos JavaScript en el arranque es menos tiempo de bootup, mejor TTI y un factor de arranque que hace honor al runtime diminuto.
La disciplina que sostiene esta palanca es medir, no adivinar. Un visor de bundle —rollup-plugin-visualizer sobre el build de Vite— muestra qué pesa de verdad en tu chunk inicial, y casi siempre la sorpresa es una dependencia transitiva que no sabías que estaba ahí: una librería de fechas entera importada por una función, un icono que arrastra su paquete completo. Optimizar el arranque no empieza por partir componentes al azar sino por mirar el mapa del bundle y cortar donde de verdad pesa. El runtime de 7 kB es el suelo; lo que construyes encima es lo que decide si tu arranque honra ese suelo o lo entierra bajo cientos de kilobytes que nadie necesitaba en el primer pintado.
Las cuatro palancas se refuerzan entre sí y trazan un orden natural de trabajo. Empiezas eligiendo bien For o Index, que es gratis y estructural; memoizas solo lo que un perfil señale como caro o compartido; sustituyes ternarios crudos por control flow declarativo cuando veas subárboles reconstruyéndose; y divides el bundle cuando el visor te muestre dónde pesa. Ninguna es un truco aislado: son la forma de mantener tu app pegada al techo que el capítulo anterior midió, aplicando en tu código la misma lógica —hacer solo el trabajo necesario— que hace rápido al framework.
For vs Index
Reordena y hay identidad de objeto: For. Longitud estable o editas el valor en su sitio: Index. Preservas lo correcto.
Memo con motivo
Memoiza solo lo caro, lo compartido o lo que corta propagacion con equals. Lo barato y unico: una funcion.
Show, no ternario
Control flow declarativo cachea la rama; el ternario crudo la reconstruye. No destructures props ni recrees signals.
lazy y Suspense
Divide el bundle por rutas y librerias pesadas. Protege el eje de arranque que el runtime de 7 kB te regala.
La diferencia más profunda entre optimizar Solid y optimizar un framework con VDOM es dónde vive el trabajo de optimización. En React optimizar es defensivo: peleas contra el re-render con memo, useMemo, useCallback y listas de dependencias, todo para evitar que el framework repita trabajo que hace por defecto. La optimización es un impuesto que pagas por el modelo. En Solid no hay re-render que combatir, así que la optimización deja de ser defensa y se vuelve elección: no evitas que el framework haga de más, sino que eliges la primitiva cuyo coste encaja con tu caso. For frente a Index no es afinar un reconciliador, es declarar si la identidad va por referencia o por posición. Memoizar no es tapar una fuga de rendimiento, es decidir dónde cachear un cálculo que de verdad lo merece. Show frente a un ternario no es un truco, es decirle al grafo que quieres conservar un subárbol. Dividir el bundle no es podar renders, es escoger qué código viaja en el arranque. Cada palanca de este capítulo es una decisión de diseño que el grano fino te deja tomar con claridad, porque el framework ya no genera el ruido que en otros modelos te obliga a optimizar a ciegas. Esto cambia el carácter del trabajo: mides menos y razonas más, porque el coste de cada primitiva es predecible y local en lugar de emergente y global. Un profesional de Solid no se pregunta “por qué se está re-renderizando esto” —la pregunta central de otros ecosistemas— sino “qué primitiva expresa exactamente el coste que quiero pagar aquí”. Cuando ese giro cuaja, optimizar deja de ser cazar patologías y pasa a ser lo que siempre debió ser: elegir bien la herramienta desde el principio, con el framework de tu lado en vez de en tu contra.
- Toma una lista de inputs editables implementada con
Fory observa que editar pierde el foco al reordenar; cámbiala aIndexy explica por qué el foco sobrevive. - Encuentra en tu código un
createMemosobre una derivación barata leída una vez, conviértelo en función normal y argumenta qué nodo del grafo eliminaste. - Añade un
equalsa un memo para que un cambio de fuente con resultado estructuralmente igual no propague, y verifica que sus consumidores no despiertan. - Sustituye un ternario crudo del JSX por
Showy comprueba con las devtools que la rama deja de reconstruirse en cada alternancia. - Convierte una librería pesada en un
import()perezoso bajoSuspensey mide la caída del tamaño del bundle inicial.