Aprende a construir listas rápidas en dashboards con 100k filas usando paginación, virtualización, filtros inteligentes y mejores consultas para que las herramientas internas sigan siendo ágiles.

Una pantalla de lista suele sentirse bien hasta que deja de hacerlo. Los usuarios empiezan a notar pequeños tartamudeos que se acumulan: el desplazamiento se corta, la página se queda un momento después de cada actualización, los filtros tardan segundos en responder y aparece un spinner tras cada clic. A veces la pestaña del navegador parece congelada porque el hilo de la UI está ocupado.
100k filas es un punto de inflexión común porque estresa todas las partes del sistema a la vez. El conjunto de datos sigue siendo normal para una base de datos, pero lo suficientemente grande como para hacer obvias las pequeñas ineficiencias en el navegador y en la red. Si intentas mostrarlo todo a la vez, una pantalla simple se convierte en una tubería pesada.
El objetivo no es renderizar todas las filas. El objetivo es ayudar a alguien a encontrar lo que necesita rápido: las 50 filas correctas, la siguiente página o una porción estrecha basada en un filtro.
Ayuda dividir el trabajo en cuatro partes:
Si cualquiera de esas partes es cara, toda la pantalla se siente lenta. Una búsqueda simple puede disparar una petición que ordena 100k filas, devuelve miles de registros y luego fuerza al navegador a renderizarlos todos. Así es como escribir se vuelve lento.
Cuando los equipos construyen herramientas internas rápido (incluso con plataformas de low-code como Koder.ai), las pantallas de lista suelen ser el primer lugar donde el crecimiento real de datos expone la brecha entre “funciona con un dataset de demo” y “se siente instantáneo todos los días”.
Antes de optimizar, decide qué significa "rápido" para esta pantalla. Muchos equipos persiguen el throughput (cargar todo) cuando los usuarios necesitan sobre todo baja latencia (ver algo actualizarse rápido). Una lista puede sentirse instantánea aunque nunca cargue las 100k filas, siempre que responda rápido al desplazamiento, orden y filtros.
Un objetivo práctico es el tiempo hasta la primera fila, no el tiempo hasta carga completa. Los usuarios confían en la página cuando ven las primeras 20 a 50 filas rápido y las interacciones se mantienen fluidas.
Elige un pequeño conjunto de números que puedas rastrear cada vez que cambias algo:
COUNT(*) y SELECTs amplios)Estos mapean los síntomas comunes. Si la CPU del navegador se dispara al desplazarte, el frontend está haciendo demasiado trabajo por fila. Si el spinner espera pero el desplazamiento va bien después, el problema suele estar en backend o la red. Si la petición es rápida pero la página sigue congelándose, casi siempre es renderizado o procesamiento pesado del lado del cliente.
Prueba un experimento simple: mantiene la UI igual, pero limita temporalmente el backend para que devuelva solo 20 filas con los mismos filtros. Si se vuelve rápido, tu cuello de botella es el tamaño de carga o el tiempo de consulta. Si sigue lento, revisa renderizado, formateo y componentes por fila.
Ejemplo: una pantalla interna de Orders se siente lenta cuando escribes en la búsqueda. Si la API devuelve 5.000 filas y el navegador las filtra en cada pulsación, escribir se retrasará. Si la API tarda 2 segundos por un COUNT en un filtro sin índice, verás espera antes de que cambie cualquier fila. Diferentes soluciones, misma queja del usuario.
El navegador es a menudo el primer cuello de botella. Una lista puede sentirse lenta incluso cuando la API es rápida, simplemente porque la página intenta pintar demasiado. La primera regla es simple: no renderices miles de filas en el DOM a la vez.
Incluso antes de añadir virtualización completa, mantén cada fila ligera. Una fila con contenedores anidados, iconos, tooltips y estilos condicionales complejos en cada celda te cuesta en cada desplazamiento y actualización. Prefiere texto plano, un par de badges pequeños y solo uno o dos elementos interactivos por fila.
La altura de fila estable ayuda más de lo que parece. Cuando cada fila tiene la misma altura, el navegador puede predecir el layout y el desplazamiento se mantiene suave. Las filas de altura variable (descripciones que hacen wrap, notas expansibles, avatares grandes) disparan mediciones extra y reflow. Si necesitas detalles adicionales, considera un panel lateral o un área expandible única, no una fila multilínea completa.
El formateo es otro coste silencioso. Fechas, moneda y trabajo pesado de strings se acumulan cuando se repiten en muchas celdas.
Si un valor no es visible, no lo calcules todavía. Cachea resultados de formateo caros y calcúlalos bajo demanda, por ejemplo cuando una fila se vuelve visible o cuando el usuario abre una fila.
Un repaso rápido que suele dar una mejora notable:
Ejemplo: una tabla interna de Invoices que formatea 12 columnas de moneda y fechas tartamudea al desplazar. Cachear los valores formateados por factura y retrasar el trabajo para filas fuera de pantalla puede hacer que se sienta instantáneo, incluso antes de trabajar más en el backend.
Virtualización significa que la tabla solo dibuja las filas que realmente puedes ver (más un pequeño buffer arriba y abajo). A medida que te desplazas, reutiliza los mismos elementos DOM y cambia los datos dentro de ellos. Eso evita que el navegador intente pintar decenas de miles de componentes de fila a la vez.
La virtualización encaja bien cuando tienes listas largas, tablas anchas o filas pesadas (avatares, chips de estado, menús de acción, tooltips). También es útil cuando los usuarios se desplazan mucho y esperan una vista continua y suave en lugar de saltar página por página.
No es magia. Unas cuantas cosas suelen causar sorpresas:
El enfoque más sencillo es aburrido: altura de fila fija, columnas predecibles y no demasiados widgets interactivos dentro de cada fila.
Puedes combinar ambas: usa paginación (o carga por cursor) para limitar lo que obtienes del servidor, y virtualización para mantener barato el render dentro del fragmento obtenido.
Un patrón práctico es obtener una página normal (a menudo 100 a 500 filas), virtualizar dentro de esa página y ofrecer controles claros para moverse entre páginas. Si usas scroll infinito, añade un indicador visible “Cargadas X de Y” para que los usuarios entiendan que no están viendo todo todavía.
Si necesitas una pantalla de lista que siga siendo usable conforme crecen los datos, la paginación suele ser la opción por defecto más segura. Es predecible, funciona bien para flujos administrativos (revisar, editar, aprobar) y soporta necesidades comunes como exportar “página 3 con estos filtros” sin sorpresas. Muchos equipos vuelven a la paginación después de probar scrolls más complejos.
El scroll infinito puede sentirse bien para navegación casual, pero tiene costes ocultos. La gente pierde la noción de dónde está, el botón de atrás no suele devolverlos al mismo punto y las sesiones largas pueden acumular memoria a medida que se cargan más filas. Un punto medio es un botón Cargar más que siga usando páginas, de modo que los usuarios se mantengan orientados.
La paginación por offset es el clásico page=10&size=50. Es simple, pero puede volverse más lenta en tablas grandes porque la base de datos puede tener que saltarse muchas filas para llegar a páginas posteriores. Además puede comportarse extraño cuando llegan filas nuevas y los items cambian de página.
La paginación por keyset (a menudo llamada cursor) pide “las siguientes 50 filas después del último elemento visto”, normalmente usando un id o created_at. Suele mantenerse rápida porque no necesita contar ni saltarse tantas filas.
Una regla práctica:
A los usuarios les gusta ver totales, pero un "contar todas las filas que coinciden" puede ser caro con filtros complejos. Opciones: cachear conteos para filtros populares, actualizar el conteo en segundo plano después de cargar la página, o mostrar un conteo aproximado (por ejemplo, "10.000+").
Ejemplo: una pantalla interna de Orders puede mostrar resultados al instante con paginación por keyset y luego completar el total exacto solo cuando el usuario deja de cambiar filtros por un segundo.
Si estás construyendo esto en Koder.ai, trata la paginación y el comportamiento del conteo como parte de la especificación de la pantalla desde el inicio, para que las consultas generadas y el estado UI no choquen más tarde.
La mayoría de pantallas de lista se sienten lentas porque empiezan demasiado abiertas: cargan todo y luego piden al usuario que lo estreche. Dale la vuelta. Empieza con valores por defecto sensatos que devuelvan un conjunto pequeño y útil (por ejemplo: Últimos 7 días, Mis elementos, Estado: Abierto) y haz de “Todo el tiempo” una elección explícita.
La búsqueda por texto es otra trampa común. Si ejecutas una consulta en cada pulsación, creas una cola de peticiones y una UI que parpadea. Debouncea la entrada de búsqueda para consultar solo después de que el usuario haga una pausa breve y cancela peticiones anteriores cuando empieza una nueva. Una regla simple: si el usuario todavía está escribiendo, no consultes el servidor aún.
Los filtros solo se sienten rápidos cuando también son claros. Muestra chips de filtro cerca de la tabla para que los usuarios vean qué está activo y lo eliminen con un clic. Mantén las etiquetas humanas, no nombres de campo crudos (por ejemplo, Owner: Sam en lugar de owner_id=42). Cuando alguien dice “mis resultados desaparecieron”, suele deberse a un filtro invisible.
Patrones que mantienen listas grandes responsivas sin complicar la UI:
Las vistas guardadas son la heroína silenciosa. En lugar de enseñar a los usuarios a construir combinaciones perfectas de filtros cada vez, dales presets que coincidan con flujos reales. Un equipo de ops podría alternar entre Pagos fallidos hoy y Clientes de alto valor. Eso puede ser un clic, entendible al instante y más fácil de mantener rápido en el backend.
Si estás construyendo una herramienta interna en un constructor guiado por chat como Koder.ai, trata los filtros como parte del flujo de producto, no como un añadido. Empieza por las preguntas más comunes y diseña la vista por defecto y las vistas guardadas alrededor de esas.
Una pantalla de lista rara vez necesita los mismos datos que una página de detalle. Si tu API devuelve todo sobre todo, pagas dos veces: la base de datos hace más trabajo y el navegador recibe y renderiza más de lo que puede usar. Query shaping es el hábito de pedir solo lo que la lista necesita ahora mismo.
Empieza devolviendo solo las columnas necesarias para renderizar cada fila. Para la mayoría de dashboards eso es un id, un par de etiquetas, un estado, un propietario y timestamps. Texto grande, blobs JSON y campos computados pueden esperar hasta que el usuario abra una fila.
Evita joins pesados para la primera pintura. Los joins están bien cuando usan índices y devuelven resultados pequeños, pero se vuelven caros cuando unes varias tablas y luego ordenas o filtras sobre los datos unidos. Un patrón simple: obtén la lista de una tabla rápido y carga detalles relacionados bajo demanda (o por lotes para las filas visibles únicamente).
Mantén las opciones de orden limitadas y ordena por columnas indexadas. "Ordenar por cualquier cosa" suena útil, pero a menudo fuerza ordenaciones lentas en datasets grandes. Prefiere unas pocas opciones previsibles como created_at, updated_at o status y asegúrate de que esas columnas estén indexadas.
Ten cuidado con agregaciones del lado servidor. COUNT(*) sobre un conjunto filtrado grande, DISTINCT en una columna amplia o cálculos de páginas totales pueden dominar tu tiempo de respuesta.
Un enfoque práctico:
COUNT y DISTINCT como opcionales y cachea o aproxima cuando sea posibleSi construyes herramientas internas en Koder.ai, define una consulta ligera para la lista separada de la consulta de detalles en modo planificación, para que la UI siga siendo ágil a medida que crecen los datos.
Si quieres una pantalla de lista que se mantenga rápida con 100k filas, la base de datos tiene que hacer menos trabajo por petición. La mayoría de listas lentas no son “demasiados datos”, son el patrón de acceso equivocado.
Empieza con índices que coincidan con lo que tus usuarios realmente hacen. Si tu lista normalmente se filtra por status y se ordena por created_at, necesitas un índice que soporte ambos, en ese orden. Si no, la base de datos puede escanear muchas más filas de lo esperado y luego ordenarlas, lo que se vuelve caro rápido.
Arreglos que suelen dar las mayores mejoras:
tenant_id, status, created_at).OFFSET profundos. OFFSET hace que la BD recorra muchas filas solo para saltarlas.Un ejemplo simple: una tabla interna de Orders que muestra nombre de cliente, estado, importe y fecha. No hagas join con cada tabla relacionada ni traigas las notas completas del pedido para la vista de lista. Devuelve solo las columnas usadas en la tabla y carga el resto en una petición separada cuando el usuario haga clic en un pedido.
Si construyes con una plataforma como Koder.ai, mantén esta mentalidad aunque la UI se genere desde chat. Asegúrate de que los endpoints API generados soporten paginación por cursor y campos selectivos, para que el trabajo en la BD sea predecible a medida que la tabla crece.
Si una página de lista se siente lenta hoy, no empieces reescribiendo todo. Comienza por definir cómo es el uso normal y luego optimiza ese camino.
Define la vista por defecto. Elige filtros por defecto, orden y columnas visibles. Las listas se vuelven lentas cuando intentan mostrarlo todo por defecto.
Elige un estilo de paginación que coincida con el uso. Si los usuarios principalmente revisan las primeras páginas, la paginación clásica está bien. Si la gente salta muy profundo (página 200+) o necesitas rendimiento estable sin importar la profundidad, usa paginación por keyset (basada en un orden estable como created_at más un id).
Añade virtualización para el cuerpo de la tabla. Incluso si el backend es rápido, el navegador puede bloquearse al renderizar demasiadas filas a la vez.
Haz que la búsqueda y los filtros se sientan instantáneos. Debouncea la escritura para no disparar peticiones en cada pulsación. Mantén el estado de filtros en la URL o en un store compartido para que refresh, el botón atrás y compartir funcionen de forma fiable. Cachea el último resultado exitoso para que la tabla no parpadee vacía.
Mide y luego afina consultas e índices. Registra tiempos de servidor, tiempo en la BD, tamaño de payload y tiempo de render. Luego recorta la consulta: selecciona solo las columnas que muestras, aplica filtros pronto y añade índices que coincidan con tu filtro + orden por defecto.
Ejemplo: un dashboard de soporte con 100k tickets. Por defecto muestra Abiertos, asignados a mi equipo, ordenados por más recientes, muestra seis columnas y solo obtiene id, asunto, asignado, estado y timestamps. Con paginación por keyset y virtualización, mantienes la base de datos y la UI predecibles.
Si construyes herramientas internas en Koder.ai, este plan encaja bien con un flujo iterar-y-comprobar: ajusta la vista, prueba desplazamiento y búsqueda, y luego afina la consulta hasta que la página se mantenga ágil.
La forma más rápida de romper una pantalla de lista es tratar 100k filas como una página normal de datos. La mayoría de dashboards lentos caen en unas trampas previsibles.
Una grande es renderizar todo y ocultarlo con CSS. Aunque parezca que solo 50 filas son visibles, el navegador sigue pagando el coste de crear 100k nodos DOM, medirlos y repintar al desplazar. Si necesitas listas largas, renderiza solo lo que el usuario puede ver (virtualización) y mantén simples los componentes de fila.
La búsqueda también puede arruinar el rendimiento silenciosamente cuando cada pulsación dispara un escaneo completo de la tabla. Eso ocurre cuando los filtros no tienen índice, cuando buscas en demasiadas columnas o cuando haces consultas de tipo contains en campos de texto enormes sin un plan. Una buena regla: el primer filtro al que recurre el usuario debe ser barato en la BD, no solo conveniente en la UI.
Otro problema común es traer registros completos cuando la lista solo necesita resúmenes. Una fila de lista suele necesitar 5–12 campos, no todo el objeto, no descripciones largas y no datos relacionados. Traer datos extra aumenta trabajo en la BD, tiempo de red y parsing en el frontend.
Exportar y totales pueden congelar la UI si los calculas en el hilo principal o esperas una petición pesada antes de responder. Mantén la UI interactiva: inicia exportaciones en segundo plano, muestra progreso y evita recalcular totales en cada cambio de filtro.
Finalmente, demasiadas opciones de orden pueden salir mal. Si los usuarios pueden ordenar por cualquier columna, acabarás ordenando grandes conjuntos en memoria o forzando a la BD a planes lentos. Mantén los ordenes a un conjunto pequeño de columnas indexadas y haz que el orden por defecto coincida con un índice real.
Chequeo rápido:
Trata el rendimiento de listas como una característica de producto, no como un ajuste puntual. Una pantalla de lista es rápida solo cuando se siente rápida mientras personas reales se desplazan, filtran y ordenan con datos reales.
Usa esta checklist para confirmar que arreglaste lo correcto:
Una comprobación simple: abre la lista, desplázate durante 10 segundos y luego aplica un filtro común (por ejemplo Estado: Abierto). Si la UI se congela, el problema suele ser renderizado (demasiadas filas DOM) o una transformación pesada del lado cliente (ordenar, agrupar, formatear) ocurriendo en cada actualización.
Próximos pasos, en orden, para no saltar entre arreglos:
Si construyes esto con Koder.ai (koder.ai), empieza en Planning Mode: define columnas exactas de la lista, campos de filtro y la forma de la respuesta primero. Luego itera usando snapshots y revierte cuando un experimento ralentice la pantalla.
Cambia el objetivo de “cargarlo todo” a “mostrar las primeras filas útiles rápido”. Optimiza para el tiempo hasta la primera fila y para que las interacciones (filtrar, ordenar, desplazarse) sean fluidas, incluso si nunca se cargan las 100k filas completas.
Mide el tiempo hasta la primera fila tras cargar o cambiar un filtro, el tiempo para que filtros/orden muestren resultados, el tamaño de la respuesta, consultas lentas en la base (especialmente selects amplios y COUNT(*)) y picos en el hilo principal del navegador. Esos números coinciden con lo que los usuarios perciben como “lag”.
Limita temporalmente la API para que devuelva solo 20 filas con los mismos filtros y orden. Si va rápido, el coste está en la consulta o el tamaño de la respuesta; si sigue lento, el cuello de botella suele ser el render, el formateo o el trabajo por fila en el cliente.
No renderices miles de filas en el DOM a la vez, mantén simples los componentes de fila y prefiere alturas de fila fijas. Además, evita hacer formateos caros para filas fuera de pantalla; calcula y cachea formateos solo cuando la fila sea visible o esté abierta.
La virtualización mantiene montadas solo las filas visibles (más un pequeño buffer) y reutiliza elementos DOM al desplazarse. Vale la pena cuando los usuarios se desplazan mucho o las filas son “pesadas”, pero funciona mejor con altura de fila constante y un diseño de tabla predecible.
La paginación suele ser la opción más segura para flujos administrativos porque mantiene a los usuarios orientados y limita el trabajo del servidor. El scroll infinito puede funcionar para navegación casual, pero complica la navegación y el uso de memoria a menos que gestiones el estado y los límites con cuidado.
La paginación por offset es más simple (page=10&size=50) pero puede volverse lenta en páginas profundas porque la BD tiene que saltarse muchas filas. La paginación por keyset (cursor) suele mantenerse rápida porque continúa desde el último registro visto y evita el coste de saltarse filas.
No dispares una petición en cada pulsación. Debouncea la búsqueda, cancela solicitudes en curso cuando llega una nueva y por defecto usa filtros que reduzcan los resultados (por ejemplo: últimos 7 días, Mis elementos) para que la primera consulta sea pequeña y útil.
Devuelve solo los campos que la lista realmente muestra: id, etiqueta, estado, propietario y timestamps suelen ser suficientes. Mueve textos largos, blobs JSON y datos relacionados a una petición de detalle para que la primera pintura sea ligera y predecible.
Haz que el filtro por defecto y el orden reflejen el uso real, y añade índices que soporten exactamente ese patrón (a menudo índices compuestos). Trata el total exacto como opcional: cachea recuentos, pré-calcula o muestra aproximaciones para no bloquear la respuesta principal.