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.
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.
Memoizar 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 bastaconst total = () => a() + b();// Justifica el memo: calculo caro Y compartido por varios consumidoresconst 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.
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 reactividadfunction 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 grafofunction 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 mostrarloconst 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.