El coste de reconciliación: mover vs recrear
For y Index evitan repintar la lista entera, pero por mecanismos opuestos: For difea por referencia y mueve nodos existentes; Index difea por longitud y actualiza signals en su sitio. Mover un nodo preserva el estado invisible que cuelga de él; recrearlo lo destruye. Esta lección disecciona ambos perfiles de coste.
La razón de que exista For en lugar de un simple array.map es el coste. Un map reconstruye todos los nodos cada vez que el array cambia; For calcula el delta y solo toca lo que se movió, nació o murió. Index va aún más lejos para su terreno: no mueve nada, empuja valores por signals. Y bajo ambos late una asimetría física del DOM: mover un nodo conserva todo lo que cuelga de él, mientras que recrearlo lo aniquila. Entender qué estrategia mueve y qué estrategia recrea es entender por qué tu lista parpadea o fluye.
- Entender que
Formueve nodos existentes en vez de recrearlos, y por qué eso importa. - Ver qué estado invisible destruye una recreación: foco, selección, scroll, transiciones.
- Comparar el perfil de coste de
For, que mueve, eIndex, que actualiza en su sitio. - Reconocer el antipatrón de
array.mapen el JSX y qué lo hace caro.
Difear el delta, no repintar la lista
Ni For ni Index re-ejecutan el mapeo entero cuando el array cambia. For corre un diff keyed que produce el conjunto mínimo de operaciones —crear, mover, eliminar— usando la referencia como clave; los algoritmos modernos minimizan el número de movimientos buscando la subsecuencia estable más larga. Index compara longitudes y, para cada posición que ya existía, se limita a escribir en su signal. En ambos casos, las filas intactas no re-ejecutan su callback ni su ámbito: su coste de actualización es exactamente cero.
Compáralo con lo que ocurre si escribes el mapeo a mano en el JSX:
// ANTIPATRON: sin keying, reconstruye TODO el subarbol en cada cambio
<ul>
{items().map((it) => <li>{it.texto}</li>)}
</ul>
Cada vez que items() cambia de referencia, esta expresión reevalúa el map completo, fabrica un array de li nuevos y sustituye el bloque entero: desmonta todo lo viejo y monta todo lo nuevo. Se pierde el estado de cada fila, se disparan de nuevo todos los efectos y el navegador rehace layout de la lista completa. For existe precisamente para reemplazar ese borrón y cuenta nueva por un diff quirúrgico.
La versión correcta delega ese trabajo en For, que mantiene la correspondencia entre cada objeto y su nodo entre actualizaciones:
// CORRECTO: For difea por referencia y solo toca el delta
<ul>
<For each={items()}>
{(it) => <li>{it.texto}</li>}
</For>
</ul>
La diferencia no es de estilo: es la distancia entre reconstruir N nodos y reubicar unos pocos. En una lista que se ordena, se filtra o se pagina, ese contraste decide si la interfaz responde al instante o tartamudea en cada cambio.
Mover preserva, recrear destruye
Un nodo del DOM transporta estado que no está en tus datos: el foco y la selección de texto, la posición de scroll interna, el valor de un input no controlado, una transición CSS en curso, la reproducción de un <video>, la composición del IME. Ese estado vive en el elemento, no en el modelo. Cuando For mueve un nodo con insertBefore, todo eso viaja intacto. Cuando una estrategia equivocada lo recrea —lo elimina y crea otro—, todo eso se reinicia a cero.
flowchart LR N[nodo DOM de una fila] --> A[foco y seleccion de texto] N --> B[posicion de scroll interna] N --> C[valor de input no controlado] N --> D[transicion CSS en curso] N --> E[reproduccion de video o audio] M[For mueve el nodo] -->|conserva todo| N R[recrear el nodo] -->|reinicia todo| Z[estado invisible perdido] style M fill:#a6e3a1,color:#11111b style R fill:#f38ba8,color:#11111b style Z fill:#f38ba8,color:#11111b
Este es el motivo profundo por el que la elección For frente a Index no es cosmética. No estás optimizando milisegundos: estás decidiendo si el foco del usuario sobrevive a un reordenamiento, si un vídeo sigue reproduciéndose cuando su fila sube, si una animación de entrada no se corta a la mitad. La reconciliación correcta es la que mueve el nodo que ya contiene ese estado, en vez de tirarlo y empezar de nuevo.
Un caso concreto lo hace tangible. Imagina filas con un input sin controlar y un vídeo, y un botón que reordena la lista:
<For each={ordenadas()}>
{(fila) => (
<li>
<input placeholder="nota" /> {/* conserva foco y texto al reordenar */}
<video src={fila.clip} /> {/* sigue reproduciendo al moverse */}
</li>
)}
</For>
Al reordenar, For mueve estos li con insertBefore: el input mantiene el texto sin guardar y el vídeo no se reinicia. La misma escena con un map en crudo vaciaría el input y rebobinaría el vídeo en cada orden, porque cada fila sería un elemento recién fabricado.
For frente a Index en coste
Los dos evitan el repintado total, pero sus perfiles son distintos y complementarios:
For: mueve nodos
Diff por referencia en tiempo lineal. Ante un reordenamiento, reubica los nodos existentes y solo crea o destruye en las inserciones y borrados. Preserva un ámbito reactivo y estado por objeto. Ideal cuando el orden cambia.
Index: actualiza en su sitio
Compara longitudes y escribe en los signals de las posiciones existentes. Nunca mueve un nodo; solo crea o destruye en los extremos al crecer o menguar. Ideal cuando las posiciones son estables y cambian los valores.
La regla mental que decide entre ambos vuelve a ser la pregunta de identidad: si la fila debe seguir a un objeto por sus saltos de posición, quieres que el nodo se mueva con él, y eso es For. Si la fila debe quedarse en su sitio mientras su contenido cambia, quieres que el nodo se ancle y solo mute su valor, y eso es Index. Elegir mal no solo cuesta rendimiento: cuesta el estado invisible del nodo, que es a menudo lo que el usuario está tocando en ese instante.
En términos de complejidad, Index actualiza una celda en tiempo constante —escribe un signal— mientras For paga un recorrido lineal para diferenciar. Pero el asintótico engaña: el término dominante en una UI no es el diff, es el trabajo del DOM que cada estrategia evita o provoca. For gasta un poco más en decidir para gastar mucho menos en construir; Index no decide nada y por eso es imbatible cuando de verdad solo cambian valores.
Ninguno de los dos, conviene insistir, re-ejecuta el cuerpo de las filas que no cambian. Esa es la propiedad que ambos comparten frente al map ingenuo, y la que hace del control flow de Solid algo cualitativamente distinto de repintar una lista.
Aun cuando una fila no tenga foco ni transiciones, mover gana. Recrear implica ejecutar de nuevo el callback, construir nodos, cablear sus bindings reactivos y registrar sus efectos; luego el navegador debe parsear, insertar y recalcular estilo y layout de todo lo nuevo. Mover reutiliza el nodo ya construido y ya suscrito: es una reubicación en el árbol, no una reconstrucción. El diff keyed paga un pequeño coste de comparación para ahorrarse el coste, mucho mayor, de fabricar DOM.
El algoritmo: mínimo de movimientos
For no reubica nodos a lo bruto. Su diff conserva el máximo de posiciones ya correctas y solo mueve lo imprescindible, apoyándose en una idea equivalente a la subsecuencia creciente más larga: las filas que ya guardan el orden relativo correcto no se tocan, y solo las que lo rompen se desplazan. Ante un reordenamiento parcial, el número de operaciones del DOM es proporcional a lo que de verdad cambió de sitio, no a la longitud de la lista.
// [A B C D E] -> [A C B D E]
// solo una fila se mueve; A D E permanecen intactas, sin re-ejecutar nada
<For each={items()}>{(it) => <Fila dato={it} />}</For>
Considera una inserción al principio de una lista de mil filas. Con un map en crudo, las mil se desmontan y se vuelven a montar. Con For, la fila nueva se crea y las mil viejas se reconocen por referencia y se reubican un puesto, sin re-ejecutar sus cuerpos. Con Index, en cambio, la posición 0 recibe el valor nuevo y las mil siguientes reciben, cada una, el valor de su vecina anterior: mil signals actualizados. El mismo gesto tiene tres costes radicalmente distintos.
flowchart LR H[insertar al principio] --> MP[map en crudo] H --> FR[For] H --> IX[Index] MP --> MPR[desmonta y remonta toda la lista] FR --> FRR[crea uno y reubica el resto por referencia] IX --> IXR[reescribe el valor de cada posicion] style MPR fill:#f38ba8,color:#11111b style FRR fill:#a6e3a1,color:#11111b style IXR fill:#fab387,color:#11111b
Hay un reverso honesto: For paga un coste fijo por su estructura de reconciliación —memoriza el mapeo de referencia a nodo y recorre el array para diferenciar—. En listas diminutas y de valores —tres etiquetas, dos strings que nunca reordenan— esa maquinaria es sobreingeniería, y Index resulta más directo. La regla de siempre decide: si hay identidad de objeto y reordenamientos, el diff de For se amortiza con lo que ahorra en recreaciones; si no los hay, Index gana en simplicidad.
Es tentador elegir entre For e Index con un microbenchmark de milisegundos, y casi siempre es la métrica equivocada. Lo decisivo no es cuántos nanosegundos tarda el diff, sino qué se conserva: foco, selección, scroll, transiciones, efectos. Elige el componente que preserva el estado que el usuario está tocando; el rendimiento bruto de ambos sobra para listas realistas.
Si vienes del virtual DOM, la palabra reconciliación evoca comparar dos árboles de descripción. En Solid no existe ese árbol intermedio: For mantiene un mapa directo de referencia a nodo real y opera sobre el DOM en vivo. Por eso su diff es sobre punteros, no sobre estructuras, y por eso preserva identidad física en lugar de reconstruirla.
El modelo mental que hay que desalojar viene de la era del virtual DOM, donde “actualizar la UI” significaba producir una descripción nueva y dejar que un reconciliador la comparase con la anterior para aplicar parches. En Solid no hay descripción nueva que comparar: el DOM real es el único estado, y reconciliar una lista es decidir, para cada nodo que ya existe, si se queda, se mueve o se destruye. For toma esa decisión por referencia y Index por posición, pero ambos comparten una ética común: conservar el máximo de trabajo ya hecho. Un nodo ya construido, ya suscrito y ya poblado de estado invisible es un activo caro; la reconciliación buena lo preserva y solo lo reubica, mientras la mala lo tira a la basura y reconstruye un gemelo vacío. Cuando interiorizas que cada recreación innecesaria destruye foco, scroll, animaciones y efectos —y no solo gasta CPU—, dejas de ver For e Index como dos formas de “pintar una lista” y empiezas a verlos como dos políticas de conservación de nodos. Esa mirada es la que te hace elegir bien sin pensarlo: no preguntas cuál es más rápido, preguntas cuál conserva lo que el usuario no quiere perder.
- Monta una lista de objetos con
For, pon el foco en un input de la tercera fila y reordena el array. Comprueba que el foco viaja con la fila. - Cambia la implementación a
items().map(...)en crudo y repite: observa que el foco se pierde y que todos los efectos de fila se vuelven a ejecutar. - Añade una transición CSS de entrada a cada fila y reordena bajo
For; confirma que las filas que solo se mueven no re-disparan la animación. - Con
Index, cambia el valor de una fila que tiene el foco en su input y verifica que el foco permanece porque el nodo no se recrea. - Explica, en términos de “mover vs recrear”, por qué
Forconserva el estado del DOM que unmapen crudo destruye. - Con una lista de mil elementos, inserta uno al principio con
Fory conIndex; razona por qué el coste real difiere pese a que solo añadiste uno.