Lección 03

¿Por qué 5.000 filas ahogan al navegador?

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.

01Qué es: el DOM como lista de piezas

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.

La ventana monta tres filas; el lienzo finge tener las 5.000 La ventana: lo único visible aquí, tres filas montadas altura = 5.000 × 36 px el scroll cree que hay 5.000 translateY(i × 36px) mueve sin rehacer el layout el resto son datos, no nodos
El marco no se mueve; el contenido sí. Lo que sale de la ventana se desmonta y deja de costar.

02Por qué sucede: el costo está en tenerlas puestas

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.

La idea que hay que llevarse

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.

5.000filas en la lista
~50.000nodos montados
~40nodos con ventana

03Cómo se ataca: la ventana del tren

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 }
Nodos montados: 5.000 filas frente a la ventana 5.000 filas montadas ~40 filas en la ventana Recalculate Style tras filtrar cientos de ms Recalculate Style tras filtrar ~10-20 ms
La rejilla de la izquierda no es adorno: son las piezas que el navegador revisa en cada cambio. La derecha revisa cuarenta.
Una fila se calcula con una multiplicación; la celda obliga a recorrer la rejilla una fila = una unidad alto = 36 px · y = i × 36 tres filas, tres posiciones una celda = otra cuenta 12 posiciones, 12 anchos la rejilla se recorre completa
La fila se mueve como bloque y su posición es una multiplicación. Si bajas a la celda, cada una suma su propia cuenta y los anchos dependen entre sí.
Lo que no funciona

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.

04Cómo diagnosticarlo

El diagnóstico cabe en una pregunta: ¿cuántos nodos hay montados cuando la página se siente lenta?

HerramientaQué se miraCó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.
Para decirlo en la entrevista

“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?

¿Para qué sirve el lienzo de altura total (5.000 × 36 px) si dentro solo hay 40 filas?

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é se virtualizan filas completas y no cada celda por separado?