Una lista no pesa por lo que ocupa en pantalla, sino por cuántas piezas tiene montadas. Aquí está la cuenta, el truco de la ventana y cómo comprobarlo.
El DOM (Document Object Model, el modelo de objetos del documento) es la lista de piezas que el navegador tiene montadas para tu página. Cada etiqueta se vuelve un nodo: un objeto con su texto, sus clases y una caja con posición y tamaño. Montar un nodo es crearlo, insertarlo en el árbol y darle geometría. El navegador no guarda una foto de lo que se ve: guarda el árbol completo, esté en pantalla o no.
Una tabla de 5.000 filas con 8 celdas no son 5.000 piezas: son 5.000 filas, 40.000 celdas y lo que viva dentro, del orden de 50.000 nodos. Esa es la escala real: en la maqueta son 5.000 filas; en el navegador, 50.000 objetos que alguien mantiene.
El scroll es barato porque no cambia nada: desplaza una capa que ya estaba pintada. Por eso una lista enorme puede scrollearse con fluidez y aun así sentirse rota.
El problema aparece cuando algo cambia. Filtras, ordenas o marcas una fila: el navegador invalida estilo y geometría y recalcula nodo por nodo. Con 50.000 nodos montados eso son cientos de milisegundos, y mientras corre, el hilo principal (el único lugar donde el navegador atiende clics y pintado) no hace otra cosa. La página no está lenta: está ocupada.
El costo no está en mirar: está en tener. El scroll perdona porque no toca nada; en cuanto tocas una fila, pagas por todas las que dejaste montadas, aunque estén a 3.000 píxeles.
Piensa en la ventana de un tren: el marco está fijo y el paisaje se desliza por delante. No llevas el paisaje contigo y, al pasar la estación, lo que viste ya no está en tu ventana.
Virtualizar es eso, con tres piezas. Un lienzo alto falso: un contenedor de
5.000 × 36 = 180.000 px para que la barra de scroll represente la lista entera. Montar solo
la ventana: con la posición del scroll, una división y una multiplicación dicen qué filas
deberían verse (unas 40) y solo esas se crean. Y mover con transform:
translateY(i × 36px) desplaza sin recalcular el layout, a diferencia de cambiar
top o la altura.
El overscan son los platos preparados: sacas dos o tres de más para que el siguiente cliente no espere. Montas unas filas extra arriba y abajo para que un scroll veloz no muestre huecos blancos. Si te pasas, llenas la mesa sin necesidad.
¿Por qué filas y no celdas? La fila tiene una sola altura y una sola posición: la cuenta es una multiplicación. Con celdas hay una posición por celda, el ancho de cada columna depende de las demás y la rejilla se calcula entera. Bajas nodos y multiplicas contabilidad.
El precio es un contrato: altura fija. Con filas de dos líneas hay que medirlas, guardar sus alturas y estimar las que no se vieron; la barra se vuelve aproximada y salta al arrastrarla. Casi siempre conviene recortar el texto a una línea.
// Ventana mínima: lienzo que miente y filas movidas con transform const ALTO = 36; // el contrato: todas las filas miden igual const OVERSCAN = 6; // platos de más, arriba y abajo const visibles = Math.ceil(viewport.clientHeight / ALTO); function pintar(filas, scrollTop) { // 1. el scroll cree que hay 5.000 filas lienzo.style.height = filas.length * ALTO + 'px'; const desde = Math.max(0, Math.floor(scrollTop / ALTO) - OVERSCAN); const extra = OVERSCAN * 2; const hasta = Math.min(filas.length, desde + visibles + extra); capa.replaceChildren(); // 2. se desmonta lo que salió for (let i = desde; i < hasta; i += 1) { const fila = document.createElement('div'); fila.className = 'fila'; // 3. mover no obliga a recalcular el layout fila.style.transform = `translateY(${i * ALTO}px)`; fila.textContent = filas[i].nombre; capa.append(fila); } } // .fila { position: absolute; left: 0; right: 0; height: 36px }
Esconder filas con display: none no las quita del árbol: el estilo se sigue recalculando y siguen ocupando memoria. Solo te ahorras layout y pintado.
El diagnóstico cabe en una pregunta: ¿cuántos nodos hay montados cuando la página se siente lenta?
| Herramienta | Qué se mira | Cómo se ve el síntoma |
|---|---|---|
| Elements › árbol del DOM | Cuántos nodos cuelgan de la lista: $0.children.length en la consola. |
5.000 tr y decenas de miles de nodos. Abrir el árbol tarda segundos. |
| Performance › pista Main | Las tareas largas, con un triángulo rojo en la esquina de la barra. | Una tarea de 300-800 ms al filtrar, en lugar de muchas pequeñas. |
| Performance › Recalculate Style y Layout | El ancho de las barras moradas dentro de esa tarea. | Recalculate Style de cientos de ms y un Layout casi igual: se recalcula lo que no se ve. |
| Memory › heap snapshot | Nodos retenidos y memoria, con dos capturas y una acción en medio. | Decenas de MB en objetos del DOM; si sube tras cada filtro, además hay fuga. |
“A long list is slow because of how many nodes are mounted, not because of how far you scroll.”
“I keep a tall spacer so the scrollbar tells the truth, and mount only the window plus a small overscan, positioned with transform.”
“I virtualize rows, not cells: one height and one position per item keeps the window math cheap.”
Comprueba que lo tienes
Cuatro preguntas. Al comprobar se marca la lección como completada.
La lista de 5.000 filas scrollea fluida, pero al filtrar se congela medio segundo. ¿Qué explica mejor la diferencia?
Por qué: el scroll solo desplaza una capa ya pintada; filtrar invalida estilo y posición de cada nodo montado. Si el costo viniera de la red, se vería como espera en Network.
¿Para qué sirve el lienzo de altura total (5.000 × 36 px) si dentro solo hay 40 filas?
Por qué: si midiera solo lo montado, la barra sería diminuta y el scroll acabaría en la fila cuarenta: el resto sería inalcanzable. La altura falsa es lo único completo, y no crea nodos.
Filas de altura variable: unas con dos líneas de texto y otras con una. ¿Cuál es la consecuencia más honesta de virtualizarlas?
Por qué: la técnica entera depende de calcular la posición con una multiplicación. Sin alturas conocidas hay que medir, guardar y estimar, así que la barra pasa a ser aproximada y se recoloca cuando la medición contradice la estimación.
¿Por qué se virtualizan filas completas y no cada celda por separado?
Por qué: una fila da una altura y una posición, y eso se resuelve con una multiplicación. Con celdas hay una posición por celda, los anchos dependen unos de otros y la rejilla se recorre completa.