Publicaste una lista. Funcionó en staging. Luego llegaron usuarios reales: alguien perdió su sitio después de pulsar “Atrás”, la analítica se hundió, los tickets de soporte mencionaban “sigue cargando para siempre” y SRE pregunta por qué un endpoint ahora representa la mitad de las IOPS de lectura.
La paginación y el desplazamiento infinito no son “opciones de UX”. Son decisiones de sistemas distribuidos con un sombrero de UI. Elegir la equivocada castigará a los usuarios, a tu base de datos, a tu caché y a tu rotación on-call—a menudo todo a la vez.
Toma la decisión como un operador, no como un diseñador
La paginación y el desplazamiento infinito optimizan cosas distintas. Si eliges por gusto, acabarás optimizando para la persona que más grita en la sala. Elige según la intención del usuario, la recuperación de errores y el coste operativo.
La intención del usuario decide la opción por defecto
- Usuarios buscando algo específico (productos, tickets, cuentas, docs): por defecto, paginación. La gente quiere puntos de referencia: números de página, URLs estables, “Atrás” que funcione y una sensación de progreso.
- Usuarios navegando para entretenerse (feeds, galerías de inspiración, timelines sociales): el desplazamiento infinito puede funcionar porque el “siguiente” importa menos que el “ahora”.
- Usuarios comparando ítems (compras, dashboards): paginación, o un híbrido con “Cargar más” y estado persistente. Las comparaciones requieren volver a un punto anterior sin perder contexto.
Realidades operativas que deben influir en la UX
El desplazamiento infinito no es “sin páginas”. Es muchas páginas pequeñas solicitadas en secuencia, lo que significa:
- Más peticiones de red durante una sesión.
- Más presión de memoria en el cliente a menos que virtualices.
- Cacheo más difícil si tu API no usa cursores.
- Atribución analítica más complicada si no lo planificas.
- Depuración más difícil porque un solo “scroll” puede tocar múltiples servicios.
La paginación no está “resuelta”. Aún puede ser lenta, inexacta y cara cuando se implementa como “OFFSET 90000 LIMIT 50” contra una tabla caliente. Cuando ves ese patrón de consulta, casi puedes oír al servidor de base de datos suspirar.
Una cita por la que realmente puedes regir un servicio
La esperanza no es una estrategia.
— General Gordon R. Sullivan
Si tu desplazamiento infinito depende de “esperar que el usuario no llegue a 10.000 ítems”, construiste una bomba de tiempo con una barra de desplazamiento.
Algunos hechos e historia que aún importan
- Las primeras listas web estaban paginadas mayormente por ancho de banda: el dial-up hacía imposible “cargar todo”, así que los límites de página fueron un truco de rendimiento antes que un patrón de UX.
- El desplazamiento infinito se popularizó a finales de los 2000 cuando los feeds y timelines sociales optimizaron para engagement, no para completar tareas.
- Los buscadores históricamente tuvieron problemas con el desplazamiento infinito porque el contenido sin URLs estables es difícil de rastrear e indexar; lo “bonito” puede ser invisible.
- La paginación por offset tiene carga algorítmica: offsets profundos suelen requerir escanear/omitir filas, lo que convierte “página 2000” en una caminata lenta por la base de datos.
- La paginación basada en cursores se volvió común en APIs a gran escala porque es estable bajo inserciones y borrados concurrentes—tu “siguiente página” no se reordena tanto.
- Las listas virtualizadas fueron respuesta al bloat del DOM: renderizar miles de nodos hunde los FPS y la batería; la virtualización sólo renderiza lo visible.
- El comportamiento del botón “Atrás” es un contrato histórico de UX: los navegadores enseñaron a la gente que atrás restaura el estado; el desplazamiento infinito lo rompe a menos que gestiones el historial cuidadosamente.
- El cacheo HTTP ama las URLs estables: las URLs paginadas cachean bien; “dame más del feed después del cursor X” también es cacheable, pero sólo si lo diseñas así.
Patrones que no molestan a los usuarios
Patrón 1: Paginación para listas orientadas a la intención
Usa paginación cuando al usuario le importe la posición y volver: resultados de búsqueda, tablas de administración, logs de auditoría, informes, inventario. Dales:
- Parámetros de URL estables (query + página o cursor).
- Progreso visible (números de página o “1–50 de 12,430”).
- Controles que funcionen con teclado y lectores de pantalla.
- Un “ir a la página” solo si realmente hace falta (más sobre eso más adelante).
Patrón 2: Desplazamiento infinito para navegación pasiva
El desplazamiento infinito está bien cuando el trabajo del usuario es “seguir mirando”. Pero no metas la mecánica por defecto de un clon de TikTok en un log de auditoría empresarial y lo llames moderno.
El desplazamiento infinito exitoso tiene algunas características:
- Fuerte comportamiento de “reanudar donde estaba” después de Atrás/Adelante/navegación.
- Estados de carga explícitos (y una parada definida ante errores).
- Virtualización, o tu UI se convierte en un calentador de espacio.
- Una vía de salida: acceso al footer, “Volver arriba”, “Ir a filtros” y un estado de fin de lista cuando corresponda.
Broma #1: El desplazamiento infinito es como un buffet: delicioso hasta que te das cuenta de que no encuentras la salida y tu teléfono está al 3%.
Patrón 3: “Cargar más” como un desplazamiento infinito más calmado
Si quieres el engagement del desplazamiento infinito sin el caos, envía un botón Cargar más. Es explícito, depurable y más amigable para herramientas de accesibilidad. También evita “tormentas de scroll” accidentales cuando un trackpad se vuelve entusiasta.
Patrón 4: “Paginación con prefetch” para velocidad sin perder estructura
La paginación no tiene por qué sentirse lenta. Prefetch la página siguiente cuando el usuario esté al 70–80% de la actual y luego intercambia instantáneamente al hacer clic. Mantén el límite de página para URLs y analítica, pero quita la espera.
Patrón 5: “Desplazamiento anclado” para corrección del historial
Esta es la versión adulta del desplazamiento infinito: mientras el usuario hace scroll, actualizas la URL para reflejar el ancla actual (número de página o cursor) y guardas la posición de scroll en el estado del historial. Atrás lo devuelve al lugar exacto. Es trabajo extra. También evita tickets de soporte que empiezan con “Lo perdí.”
Paginación bien hecha (UI + API)
Reglas de UI que previenen clicks de rabia
- Muestra siempre dónde está el usuario: número de página y rango de resultados. “Página 7” vence a “más cosas allá abajo”.
- Mantén el tamaño de página predecible: cambiar el número de ítems por página a mitad de la sesión rompe el mapa mental.
- Haz que “Atrás” funcione: almacena estado en la URL y restaura filtros/orden. Si tu app requiere tres clics para volver a donde estaban, construiste un laberinto.
- No abuses de los enlaces de página: muestra una ventana (por ejemplo, 1 … 6 7 8 … 200). Los usuarios no quieren ver un calendario de tu dataset.
- Permite “ir a página” solo con guardarraíles: los saltos a páginas profundas pueden ser caros e inconsistentes a menos que tu backend los soporte eficientemente.
Reglas de API: offset vs cursor, y cuándo cada uno duele
Paginación por offset (page=7, size=50) es simple y estable para datasets pequeños. Se descompone cuando:
- Tienes paginación profunda (página 500+).
- Filas se insertan/borran con frecuencia; “página 7” deriva y aparecen duplicados.
- Tu consulta DB se convierte en una operación O(n) de salto.
Paginación basada en cursor (after=cursor) es mejor para datasets grandes y cambiantes. Requiere:
- Una clave de orden estable (timestamp + ID como desempate, o una clave primaria monotónica).
- Un token cursor que codifique la “última posición vista”.
- Pensar cuidadosamente sobre filtros y ordenamientos para que los cursores sigan siendo válidos.
Diseña tu ordenamiento como si lo pensaras
La paginación por cursor es tan buena como el orden por el que paginas. “ORDER BY updated_at DESC” suena razonable hasta que recuerdas que las actualizaciones ocurren. Entonces los ítems saltan y los cursores se vuelven poco fiables. Prefiere claves de orden inmutables:
- Tiempo de creación para feeds (si el concepto es “novedad”).
- Orden por clave primaria para listas de administración (si la “estabilidad” importa más que la semántica).
- Claves compuestas para unicidad (created_at, id) para evitar duplicados con timestamps iguales.
Haz que los límites de página sean cacheables
La paginación brilla cuando puedes cachear. Si tu endpoint es “/search?q=…&page=3”, eso es una clave de caché limpia. Con cursores, las claves de caché aún pueden funcionar, pero solo si los cursores son estables y no secretos por usuario. Si son por usuario, espera tasas de hit de caché más bajas y planifica capacidad en consecuencia.
Desplazamiento infinito bien hecho (sin caos)
Regla 1: Virtualiza la lista o paga en batería y bugs
Si añades nodos DOM para siempre, eventualmente colapsarás Safari móvil, o al menos degradarás el scrolling a una presentación de diapositivas. Virtualizar significa renderizar solo los ítems visibles más un buffer, manteniendo el tamaño del DOM acotado.
Regla 2: Controla la concurrencia de solicitudes
El desplazamiento infinito dispara fetches según la posición de scroll. Sin límites de concurrencia:
- Enviarás múltiples solicitudes solapadas para el mismo cursor.
- Habrá carreras entre respuestas y reordenamiento de ítems.
- Saturarás tu backend cuando el usuario haga un fling hasta el fondo.
Usa una sola solicitud en vuelo por segmento del feed. Cancela solicitudes obsoletas. Deduplifica ítems por ID.
Regla 3: Haz que los estados de error sean terminales y recuperables
Cuando una petición falla, no le des vueltas. Muestra una opción “Reintentar”. Registra el cursor, el estado de filtros y el ID de correlación para poder reproducir el problema en el servidor. “Algo salió mal” sin contexto es la versión de los ingenieros del encogimiento de hombros.
Regla 4: Corrige la semántica del historial explícitamente
“Atrás” debería devolverte al mismo ítem. Eso requiere:
- Guardar la posición de scroll en el estado del historial (no solo en memoria).
- Actualizar la URL a medida que cambia el ancla (número de página o cursor).
- Restaurar ítems desde caché (lado cliente) o re-fetchear rápidamente.
Regla 5: Proporciona una vía de escape en el footer
El desplazamiento infinito a menudo elimina el footer, y con él navegación, enlaces de soporte y textos legales. Los usuarios seguirán necesitando eso. Dales una forma de llegar al final o proporciona un footer sticky/panel de utilidades.
Broma #2: Depurar desplazamiento infinito sin logs es como arrear gatos—excepto que los gatos son peticiones HTTP y saben dónde vives.
Patrones híbridos que ganan en el mundo real
Híbrido A: Desplazamiento infinito dentro de un límite de página
Muestras “Página 1” con 50 ítems, pero los cargas progresivamente mientras el usuario hace scroll, y mantienes la URL y el estado como “page=1”. Cuando el usuario llega al final, hace clic en “Página siguiente”. Esto reduce el tiempo de carga inicial y todavía da estructura.
Híbrido B: “Cargar más” con páginas numeradas en la URL
Cada “Cargar más” incrementa un contador interno de página y actualiza la URL para reflejar la última página cargada. Atrás funciona, la analítica puede atribuir engagement a páginas y el usuario sigue teniendo un scroll ininterrumpido.
Híbrido C: Navegación de dos niveles para “explorar y luego refinar”
Empieza con desplazamiento infinito para descubrimiento, pero cuando el usuario aplica filtros u ordena, cambia a paginación. Filtrar cambia la intención: la gente deja de vagar y empieza a cazar. Tu UI debe detectar ese cambio.
Rendimiento y almacenamiento: qué se rompe primero
El impuesto oculto de almacenamiento de “solo una página más”
Desde la silla de un ingeniero de almacenamiento, el desplazamiento infinito tiende a crear sesiones largas con muchas peticiones pequeñas. Eso cambia tu perfil de I/O:
- Más amplificación de lectura en la base de datos si la paginación profunda es por offset.
- Más churn de caché en el CDN o proxy reverso si los tokens de cursor no son cacheables.
- Más presión en el almacenamiento de objetos si cada tarjeta referencia múltiples imágenes y haces lazy-load agresivo sin cabeceras de cache.
Diseño de consultas backend: el asesino silencioso
Si tu API usa “OFFSET … LIMIT …” en una tabla grande, verás la latencia subir aproximadamente con la profundidad del offset. En producción, eso se convierte en picos de latencia tail. La latencia tail es lo que los usuarios recuerdan.
Las consultas con cursor suelen verse como “WHERE (created_at, id) < (last_created_at, last_id) ORDER BY created_at DESC, id DESC LIMIT 50.” Eso escala mejor, usa índices efectivamente y se comporta bajo escrituras concurrentes.
Rendimiento del lado cliente: memoria y tiempo del hilo principal
Sin virtualización, el navegador retiene cada nodo renderizado, imágenes, manejadores de eventos y estado de layout. La memoria crece. La recolección de basura se hace cara. El scrolling se vuelve errático. La CPU se despierta más a menudo, lo que destruye la batería móvil.
Límites de tasa: tu última línea de defensa
Los bugs de desplazamiento infinito pueden disparar inundaciones de tráfico accidentales. Limita la tasa por usuario y por IP. Pero hazlo con cuidado: los rate limits deben degradar con gracia (“desacelera, reintenta”) y no fallar en seco mostrando pantallas en blanco.
Observabilidad: mide lo que los usuarios sienten
Si no puedes medirlo, discutirás sobre ello. Instrumenta tanto la UI como el backend con un ID de petición compartido. Luego mide:
- Tiempo hasta los primeros ítems significativos: ¿qué tan rápido aparece contenido?
- Indicadores de scroll jank: tareas largas en el hilo principal, pérdidas de frames.
- Peticiones por sesión: el desplazamiento infinito suele aumentar llamadas; valida si vale la pena.
- Tasa de errores por cursor/página: las fallas podrían agruparse en páginas profundas por ineficiencia de consulta.
- Abandono tras carga: ¿la gente se va porque la carga es lenta o porque el contenido no es relevante?
- Éxito de la navegación Atrás: mide con qué frecuencia volver restaura la posición previa.
Un truco práctico: registra el índice del ítem más profundo alcanzado y el último ancla estable (página o cursor). Convierte “los usuarios lo odian” en “el 70% de sesiones nunca cargan más de 40 ítems, así que deja de prefetch 200.”
Tres mini-historias corporativas desde las trincheras
Mini-historia 1: El incidente causado por una suposición equivocada
Heredamos una consola interna de administración que mostraba eventos de auditoría. Un product manager pidió “desplazamiento infinito como apps modernas” porque el paginado parecía viejo. El equipo lo implementó. Se lanzó con paginación por offset bajo el capó: cada scroll hacía la misma llamada al endpoint con offset y limit.
La suposición fue: “nadie llega tan lejos con el scroll”. Era verdad para la navegación casual. No lo fue para la respuesta a incidentes. Durante una investigación de seguridad, los analistas hicieron scroll hacia atrás horas y luego días. Las consultas con offset se hicieron cada vez más profundas y la latencia aumentó. La UI reaccionó disparando más solicitudes porque el umbral de scroll seguía disparándose mientras la petición previa aún estaba pendiente.
La base de datos hizo lo que hacen las bases de datos cuando se les pide saltar una montaña de filas repetidamente: se calentó. La CPU subió. Apareció lag en replicación. De repente, la consola de administración no fue lo único afectado—otros servicios que compartían el mismo clúster empezaron a tener timeouts. On-call tuvo que limitar el endpoint y desactivar el desplazamiento infinito tras una feature flag.
La solución no fue “añadir más BD”. La solución fue paginación por cursor con un índice que casara con el orden, además de una puerta de concurrencia en el cliente (solo una petición en vuelo). También añadimos un filtro “Saltar a timestamp”, porque los investigadores no quieren hacer scroll por el martes para llegar al lunes; quieren un timestamp.
Mini-historia 2: La optimización que salió mal
Un equipo distinto intentó que un feed pareciera instantáneo prefetchando agresivamente: al cargar la página, pedir página 1, 2, 3 y 4 en paralelo. Estaban orgullosos. Sus dashboards mostraban buena latencia mediana para “primer render de página”, porque la primera página volvía rápido y el resto se cargaba en segundo plano.
Entonces los usuarios móviles empezaron a quejarse por batería y consumo de datos. Mientras tanto, la capa de caché empeoró: las peticiones de prefetch estaban personalizadas, los tokens de cursor eran específicos por usuario y la tasa de aciertos de caché cayó. El backend vio un salto en peticiones por sesión, incluso para usuarios que rebotaban a los cinco segundos.
El verdadero modo de fallo fue la coordinación. El prefetch no respetó la intención del usuario. Asumió que cada sesión sería profunda. También creó patrones de tráfico en ráfaga: cada vista de página lanzaba múltiples llamadas, aumentando la carga en horas pico de forma muy sincronizada.
La solución fue aburrida: prefetch solo una página adelante, solo después de que el usuario muestre intención (scroll más allá de un umbral), y nunca en paralelo con el render inicial. También añadimos pistas del servidor: devolver has_more y un prefetch_after_ms recomendado en la respuesta para ajuste dinámico. El feed se sintió igual. La infraestructura se calmó.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Un equipo de búsqueda e-commerce quiso desplazar infinito para aumentar el engagement. SRE estaba escéptico. El compromiso fue un rollout por etapas con guardarraíles: feature flag, canary users, presupuestos de error estrictos y un kill switch que se podía activar sin desplegar.
También hicieron el trabajo poco glamuroso: tests sintéticos que hacían scroll hasta una profundidad fija, capturaban trazas waterfall y validaban que “Atrás” restaurara la posición de scroll previa. Mantuvieron URLs de paginación incluso usando “Cargar más”, actualizando el estado de historial a medida que el usuario avanzaba.
Dos semanas tras el lanzamiento, un cambio en una dependencia del servicio de redimensionado de imágenes incrementó los tiempos de respuesta. La nueva UI de desplazamiento infinito lo amplificó porque los usuarios cargaban más imágenes por sesión. Pero como el equipo tenía métricas de “peticiones por sesión” y “tiempo hasta el siguiente lote”, vieron la regresión rápido y usaron el kill switch para revertir a navegación paginada mientras se arreglaba el servicio de imágenes.
Sin drama. Sin war room. Un plan aburrido hizo cosas aburridas, que es exactamente lo que quieres en producción.
Guía rápida de diagnóstico
Cuando los usuarios reportan “el scroll se siente roto” o “la paginación es lenta”, no empieces por discutir la UI. Encuentra el cuello de botella en tres pases.
Primero: ¿es jank del cliente o latencia de red/backend?
- Revisa trazas de rendimiento del navegador (tareas largas, thrash de layout, crecimiento de memoria).
- Revisa la cascada de peticiones: ¿las llamadas son lentas, o simplemente demasiadas?
- Comprueba si las imágenes dominan el tiempo de transferencia.
Segundo: ¿el modelo de paginación de la API está luchando con el modelo de datos?
- ¿Offset en paginación profunda? Espera scans en BD y latencia tail.
- Paginación por cursor pero orden inestable? Espera duplicados/ítems faltantes y usuarios enfadados.
- ¿Filtros no incluidos en el cursor? Espera un “siguiente” incorrecto.
Tercero: ¿el cacheo/ratelimit está haciendo algo no intencionado?
- ¿La tasa de hits de caché cayó después del rollout de infinite scroll? Probablemente tokens de cursor no cacheables o demasiado granulares.
- ¿429s subiendo? El frontend puede estar sobrefetching o reintentando agresivamente.
- ¿CDN bytes subiendo? El lazy-loading de imágenes puede estar generando demasiadas variantes únicas.
Tareas prácticas: comandos, salidas y decisiones
Estas son tareas que puedes ejecutar hoy para dejar de adivinar. Cada una incluye un comando realista, qué significa la salida y qué decisión tomar.
Task 1: Confirmar si hay consultas OFFSET profundas
cr0x@server:~$ sudo grep -E "OFFSET [1-9][0-9]{4,}" /var/log/postgresql/postgresql-15-main.log | tail -n 3
2025-12-29 09:10:02 UTC LOG: duration: 812.433 ms statement: SELECT id, created_at FROM events ORDER BY created_at DESC OFFSET 50000 LIMIT 50;
2025-12-29 09:10:03 UTC LOG: duration: 944.120 ms statement: SELECT id, created_at FROM events ORDER BY created_at DESC OFFSET 60000 LIMIT 50;
2025-12-29 09:10:04 UTC LOG: duration: 1102.009 ms statement: SELECT id, created_at FROM events ORDER BY created_at DESC OFFSET 70000 LIMIT 50;
Significado: Estás haciendo paginación por offset profunda; la latencia aumenta con el offset.
Decisión: Pasa a paginación basada en cursor o añade un filtro de tiempo/mecanismo de salto; no “optimices” añadiendo más reintentos desde la app.
Task 2: Comprobar uso de índices en la consulta paginada
cr0x@server:~$ psql -d appdb -c "EXPLAIN (ANALYZE, BUFFERS) SELECT id, created_at FROM events ORDER BY created_at DESC OFFSET 50000 LIMIT 50;"
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------
Limit (cost=28450.12..28450.25 rows=50 width=16) (actual time=801.122..801.140 rows=50 loops=1)
Buffers: shared hit=120 read=980
-> Gather Merge (cost=23650.00..28600.00 rows=120000 width=16) (actual time=620.440..796.300 rows=50050 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=120 read=980
-> Sort (cost=22650.00..22800.00 rows=60000 width=16) (actual time=580.110..590.230 rows=25025 loops=3)
Sort Key: created_at DESC
Sort Method: external merge Disk: 14560kB
-> Seq Scan on events (cost=0.00..12000.00 rows=60000 width=16) (actual time=0.220..220.300 rows=60000 loops=3)
Planning Time: 0.220 ms
Execution Time: 802.010 ms
Significado: Scan secuencial + sort + external merge: estás pagando por paginación profunda con trabajo en disco.
Decisión: Añade un índice que coincida con el orden y muévete a paginación por cursor; si debes mantener offset por ahora, limita la profundidad máxima de página.
Task 3: Detectar quejas por ítems duplicados correlacionando tokens de cursor
cr0x@server:~$ sudo grep "cursor=" /var/log/nginx/access.log | awk '{print $7}' | tail -n 5
/feed?limit=50&cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6IjIwMjUtMTItMjlUMDk6MDk6MDAuMDAwWiJ9
/feed?limit=50&cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6IjIwMjUtMTItMjlUMDk6MDk6MDAuMDAwWiJ9
/feed?limit=50&cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6IjIwMjUtMTItMjlUMDk6MDk6MDAuMDAwWiJ9
/feed?limit=50&cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6IjIwMjUtMTItMjlUMDk6MDk6MDAuMDAwWiJ9
/feed?limit=50&cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6IjIwMjUtMTItMjlUMDk6MDk6MDAuMDAwWiJ9
Significado: Mismo cursor repetido: el cliente está re-solicitando la misma página (probablemente bucle de retry o bug de concurrencia).
Decisión: Añade deduplicación de in-flight en el cliente, backoff en reintentos y idempotencia/dedupe en servidor por request ID.
Task 4: Verificar rate limiting y si el desplazamiento infinito lo está activando
cr0x@server:~$ awk '$9==429 {count++} END{print "429s:", count}' /var/log/nginx/access.log
429s: 384
Significado: Usuarios están siendo throttled. A menudo es autoinfligido por overfetch + retry.
Decisión: Ajusta umbrales y reintentos en frontend; añade pistas del servidor (retry-after) y asegúrate de que el rate limiting sea por usuario, no global.
Task 5: Comprobar si las respuestas son cacheables
cr0x@server:~$ curl -sI "http://app.internal/search?q=router&page=2" | egrep -i "cache-control|etag|vary"
Cache-Control: public, max-age=60
ETag: "9a1d-17c2f2c"
Vary: Accept-Encoding
Significado: Bien: respuesta cacheable con ETag; las URLs de paginación probablemente cachean bien.
Decisión: Mantén las URLs de paginación estables; para infinite scroll/cursores, considera cursores cacheables y TTLs cortos.
Task 6: Detectar tokens de cursor personalizados que matan la tasa de hit de caché
cr0x@server:~$ curl -sI "http://app.internal/feed?limit=50&cursor=abc" | egrep -i "cache-control|vary|set-cookie"
Cache-Control: private, no-store
Vary: Authorization
Set-Cookie: session=...
Significado: La respuesta es explícitamente no cacheable y varía por Authorization.
Decisión: Acepta el coste (planifica capacidad), o rediseña: separa datos personalizados del contenido público de la tarjeta; cachea lo que puedas.
Task 7: Encontrar tormentas inducidas por la UI (peticiones por minuto)
cr0x@server:~$ sudo awk '{print $4}' /var/log/nginx/access.log | cut -d: -f1,2 | sort | uniq -c | tail -n 5
812 [29/Dec/2025:09:09
945 [29/Dec/2025:09:10
990 [29/Dec/2025:09:11
1044 [29/Dec/2025:09:12
1202 [29/Dec/2025:09:13
Significado: El tráfico está subiendo rápido. Si coincide con un despliegue frontend, sospecha triggers/prefetch de infinite scroll.
Decisión: Rollback o desactivar la feature flag; luego arregla umbrales y límites de concurrencia.
Task 8: Confirmar crecimiento de memoria del cliente vía RSS de procesos node (SSR o BFF)
cr0x@server:~$ ps -o pid,rss,cmd -C node | head -n 5
PID RSS CMD
3221 485000 node server.js
3380 512300 node server.js
Significado: RSS es grande y está creciendo; podría ser SSR guardando demasiado estado de lista por sesión.
Decisión: Deja de retener estado de lista por sesión en el servidor; cachea templates o fragmentos, no historial de scroll por usuario.
Task 9: Medir latencia tail de la API en el edge (stats proxy p95/p99)
cr0x@server:~$ sudo awk '$7 ~ /^\/feed/ {print $NF}' /var/log/nginx/access.log | tail -n 5
rt=0.112
rt=0.984
rt=1.203
rt=0.221
rt=1.544
Significado: Los tiempos de respuesta varían mucho; el p99 parecerá “la app está rota” incluso si la mediana está bien.
Decisión: Arregla la forma de la consulta backend; añade timeouts + UI de fallback; reduce el payload por petición.
Task 10: Confirmar presión de I/O de disco durante paginación profunda
cr0x@server:~$ iostat -xm 1 3
Linux 6.5.0 (db01) 12/29/2025 _x86_64_ (8 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.40 0.00 6.10 9.80 0.00 71.70
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s w_await aqu-sz %util
nvme0n1 520.0 41280.0 0.0 0.00 18.20 79.38 40.0 2048.0 3.10 9.55 88.00
Significado: Alto I/O de lectura y alta utilización; la paginación profunda puede forzar lecturas de disco y sorts.
Decisión: Arregla índices y plan de consulta; añade caché; considera réplicas de lectura solo después de saneada la forma de la consulta.
Task 11: Verificar CDN/caché de objetos para imágenes en listas de scroll
cr0x@server:~$ curl -sI "http://cdn.internal/images/item123?w=640" | egrep -i "cache-control|age|etag"
Cache-Control: public, max-age=31536000, immutable
ETag: "img-7c21"
Age: 18422
Significado: Buen caché. El desplazamiento infinito seguirá cargando muchas imágenes, pero las vistas repetidas no volverán a descargar tanto.
Decisión: Mantén URLs de imagen estables e inmutables; evita generar URLs únicas por petición.
Task 12: Detectar errores de “spinner infinito” en logs cliente enviados al servidor
cr0x@server:~$ sudo grep -E "feed_load_failed|pagination_fetch_error" /var/log/app/client-events.log | tail -n 5
2025-12-29T09:11:22Z feed_load_failed cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6Ii4uLiJ9 status=504
2025-12-29T09:11:25Z feed_load_failed cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6Ii4uLiJ9 status=504
2025-12-29T09:11:28Z feed_load_failed cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6Ii4uLiJ9 status=504
Significado: 504s repetidos para el mismo cursor: timeout backend más bucle de retry en cliente.
Decisión: Añade backoff exponencial y un botón “Reintentar” visible al usuario; arregla la causa del timeout backend antes de aumentar timeouts.
Task 13: Confirmar que la API devuelve claves de ordenamiento estables para cursores
cr0x@server:~$ curl -s "http://app.internal/feed?limit=3" | jq '.items[] | {id, created_at}'
{
"id": 981223,
"created_at": "2025-12-29T09:13:01.002Z"
}
{
"id": 981222,
"created_at": "2025-12-29T09:13:00.991Z"
}
{
"id": 981221,
"created_at": "2025-12-29T09:13:00.990Z"
}
Significado: La lista expone claves estables; puedes construir un cursor sobre (created_at, id).
Decisión: Usa cursor compuesto; evita ordenar por campos mutables como updated_at para la paginación principal.
Task 14: Comprobar si existen anclas de historial HTML para SEO y navegación Atrás
cr0x@server:~$ curl -s "http://app.internal/search?q=router&page=2" | grep -Eo 'rel="(next|prev)"' | sort | uniq -c
1 rel="next"
1 rel="prev"
Significado: La página declara relaciones next/prev. Eso ayuda a los crawlers y también clarifica la estructura de navegación.
Decisión: Mantén esto para listas paginadas; para desplazamiento infinito, expón URLs paginadas equivalentes por debajo del capó.
Errores comunes: síntomas → causa raíz → solución
1) “El botón Atrás me lleva arriba”
Síntoma: Usuarios hacen clic en un ítem, vuelven y pierden su lugar.
Causa raíz: Posición de scroll no persistida; URL no actualizada con ancla; estado de la lista descartado.
Solución: Almacena la posición de scroll en el estado del historial; actualiza la URL con página/cursor; restaura desde ítems en caché o re-fetch rápido.
2) “Veo duplicados / faltan ítems al hacer scroll”
Síntoma: Ítems se repiten o aparecen huecos después de cargar más.
Causa raíz: Orden inestable (updated_at), cursor no ligado a clave única de orden, peticiones concurrentes en carrera o falta de dedupe.
Solución: Usa orden inmutable (created_at + id); fuerza una sola petición en vuelo; deduplica por ID en cliente y servidor.
3) “Carga para siempre” (spinner infinito)
Síntoma: El loader gira; no aparece nada nuevo; el usuario sigue haciendo scroll.
Causa raíz: Bucle de retry en 5xx/timeout; estado de fallo no expuesto; el trigger de scroll sigue disparando.
Solución: Haz los fallos terminales con “Reintentar”; añade backoff exponencial; añade comportamiento de circuito abierto; registra cursor y request ID.
4) “La página 1 es rápida, la 200 es inutilizable”
Síntoma: Navegación profunda lenta; p99 explota.
Causa raíz: Paginación por offset escaneando rangos grandes; índices compuestos faltantes.
Solución: Paginación por cursor; añade índice que coincida; limita acceso a páginas profundas; ofrece salto/búsqueda por tiempo.
5) “El scroll va entrecortado en móvil”
Síntoma: FPS bajos, respuesta táctil retrasada, dispositivo se calienta.
Causa raíz: Demasiados nodos DOM, imágenes pesadas, thrash de layout, trabajo sincronizado en eventos de scroll.
Solución: Virtualiza; usa placeholders de imagen y tamaños correctos; evita trabajo sincrónico en eventos de scroll; limita observadores.
6) “La analítica es un sinsentido tras cambiar a infinite scroll”
Síntoma: Conversiones caen (o suben) misteriosamente; atribución rota.
Causa raíz: Tracking basado en pageview no mapea a infinite scroll; faltan eventos de “ítems vistos” y de profundidad.
Solución: Rastrear eventos de exposición (ítems renderizados/visibles), profundidad alcanzada y cambios de ancla; mantener URLs actualizadas para conservar semántica.
7) “Nuestra tasa de hit de caché se desplomó”
Síntoma: Ratio de aciertos en CDN/proxy cae tras rollout.
Causa raíz: Cursores personalizados, variación por Authorization, cabeceras private/no-store.
Solución: Separa datos públicos de privados; cachea payloads de tarjetas por separado; mantén tokens de cursor determinísticos; usa TTLs cortos cuando sea seguro.
8) “Los usuarios no pueden llegar al footer”
Síntoma: Soporte dice “No encuentro contacto/avisos/ajustes”.
Causa raíz: El desplazamiento infinito eliminó el final natural de la página.
Solución: Proporciona una barra de utilidades sticky, un “Pausar carga” que permita alcanzar el footer, o una opción “Ir al footer”.
Listas de verificación / plan paso a paso
Paso a paso: elegir el patrón
- Clasifica la intención: búsqueda/comparación (paginación) vs navegación (infinite o cargar-más).
- Define “volver a donde estaba”: ¿es un requisito? Si sí, diseña anclas de historial desde el principio.
- Elige un modelo de API: basado en cursores para datasets grandes/dinámicos; offset solo para listas pequeñas y estables.
- Decide claves de ordenamiento estables: claves de orden inmutables con desempate.
- Fija un presupuesto de rendimiento: tiempo máximo al siguiente lote, max peticiones por sesión, máximo de nodos DOM.
- Planifica el cacheo: qué puede ser público, qué debe ser privado y dónde los TTL tienen sentido.
- Instrumenta la semántica analítica: exposición, profundidad, cambios de ancla, comportamiento de reintentos.
- Lanza con guardarraíles: feature flag, canary y un kill switch.
Checklist: UI de paginación que no irrita
- La URL refleja el estado (filtros/orden/página/cursor).
- Muestra recuento total o una aproximación significativa (y etiquétalo honestamente).
- Controles accesibles por teclado, gestión de foco y etiquetas ARIA.
- Siguiente/previo más ventana de páginas; no una monstruosidad de 200 enlaces.
- Prefetch de la página siguiente solo cuando no genere tráfico en ráfaga.
- Acceso a páginas profundas soportado eficientemente o deliberadamente restringido.
Checklist: desplazamiento infinito que no derrite dispositivos
- Virtualización activada; cuenta de nodos DOM acotada.
- Una sola petición en vuelo; cancela llamadas obsoletas; deduplica ítems.
- Estados de error claros con Reintentar; nada de spinners infinitos.
- Estado de historial + actualizaciones de ancla en la URL; Atrás devuelve al mismo lugar.
- Footer/navegación accesible vía elemento UI persistente.
- Presupuesto de peticiones aplicado (profundidad máxima, prefetch máximo, cargas concurrentes de media).
Checklist: requisitos backend para ambos patrones
- Índice que coincida con el orden.
- Tokens de cursor incluyen todo el contexto de ordenamiento/filtros necesarios o se rechazan de forma segura.
- La respuesta incluye
has_morey un puntero a siguiente cursor/página. - Rate limiting y reintentos están coordinados (429 con semántica Retry-After).
- Observabilidad: request IDs, cursor/página en logs y percentiles de latencia.
Preguntas frecuentes
1) ¿Siempre debo preferir desplazamiento infinito en móvil?
No. Los usuarios móviles tienen menos paciencia para cargas lentas y menos memoria para grandes DOMs. Si la tarea es buscar/comparar, la paginación (o “cargar más”) suele ser mejor.
2) ¿“Cargar más” es solo paginación perezosa?
Es paginación con un modelo de interacción más amigable. Es explícito, más fácil de hacer accesible y más fácil de depurar. Para muchos productos es el punto óptimo.
3) ¿Por qué la paginación por offset se vuelve lenta en páginas altas?
Porque la base de datos a menudo tiene que escanear/omitir muchas filas para alcanzar el offset y luego ordenar o filtrar. Incluso con índices, offsets profundos pueden forzar trabajo proporcional a lo que has saltado.
4) ¿La paginación por cursor arregla duplicados por completo?
Arregla muchas causas, pero no todas. Aún necesitas claves de ordenamiento estables y un desempate. Y aún necesitas dedupe en cliente si puedes emitir peticiones duplicadas.
5) ¿Cómo hago que el desplazamiento infinito sea amigable para SEO?
Expón URLs paginadas que representen las mismas porciones de contenido y hazlas alcanzables (server-rendered o al menos detectables). El desplazamiento infinito puede ser la experiencia cliente; la estructura rastreable debe existir.
6) ¿Los usuarios prefieren desplazamiento infinito?
Los usuarios prefieren lo que les ayuda a terminar su tarea con menos fricción. Para navegar, el infinito puede sentirse fluido. Para buscar, comparar y volver, la paginación suele ganar.
7) ¿Cuál es la forma más simple de evitar picos de tráfico por infinite scroll?
Aplica una petición en vuelo, prefetch como máximo una página adelante y exige intención de usuario (umbral de scroll) antes de prefetch. Añade backoff y limita reintentos.
8) ¿Debo mostrar el recuento total de resultados?
Si los usuarios toman decisiones basadas en alcance (“solo 23 resultados” vs “12,000 resultados”), sí. Si los conteos son caros, muestra una estimación o rangos—no mientas.
9) ¿Puedo mantener números de página con paginación por cursor?
Puedes, pero es complicado. La paginación por cursor no mapea naturalmente a saltos arbitrarios de página. Si se requieren números de página, considera almacenar cursores por página en la sesión cliente, o proveer saltos por tiempo en su lugar.
10) ¿Cuál es la opción por defecto mejor para tablas administrativas empresariales?
Paginación, con ordenamiento y filtrado server-side, y URLs estables. Añade “cargar más” solo si puedes garantizar el comportamiento de Atrás y rendimiento bajo uso profundo.
Conclusión: próximos pasos que no desperdiciarán tu trimestre
Si tu lista es una herramienta, envía paginación (o “cargar más”) con URLs estables y APIs basadas en cursores. Si tu lista es entretenimiento, el desplazamiento infinito puede estar bien—pero solo con virtualización, control de concurrencia y semántica real de historial.
Próximos pasos que rinden rápido:
- Audita tus consultas backend por uso profundo de
OFFSETy arregla la forma de la consulta antes de retocar la UI. - Decide y documenta la clave de orden para paginación y hazla inmutable con desempate.
- Añade un modelo de ancla (página/cursor) a URLs y al estado de historial para que Atrás se comporte como los usuarios esperan.
- Instrumenta profundidad, exposición, reintentos y latencia tail—luego fija un presupuesto de peticiones por sesión.
- Lanza con un kill switch. Me lo agradecerás luego, normalmente a las 2:13 a.m.