Complete TASK-052 historical UI load pass

This commit is contained in:
devRaGonSa
2026-03-23 13:44:39 +01:00
parent 68de2d955a
commit ef1a42fb80
2 changed files with 173 additions and 61 deletions

View File

@@ -0,0 +1,72 @@
# TASK-052-historical-ui-initial-load-performance-pass
## Goal
Reducir drásticamente el tiempo percibido de carga inicial de `historico.html`, evitando peticiones innecesarias al entrar, eliminando precargas agresivas y permitiendo que resumen, ranking y partidas recientes se hidraten de forma progresiva e independiente.
## Context
La página histórica ya consume snapshots rápidos, pero el comportamiento actual sigue generando una experiencia lenta:
- se lanzan varias peticiones de snapshots al entrar
- se precargan varias métricas de leaderboard demasiado pronto
- hay prefetch por interacción ligera o por estrategia demasiado agresiva
- la UI tarda en mostrar contenido útil porque espera más de la cuenta antes de hidratar bloques visibles
El objetivo de esta task es que la carga inicial sea más ligera y que el usuario vea contenido útil antes, aunque el resto de bloques sigan llegando progresivamente.
## Steps
1. Revisar `frontend/assets/js/historico.js` y la estrategia actual de carga inicial.
2. Identificar y reducir las peticiones lanzadas al entrar en la página para el servidor activo.
3. Hacer que en la carga inicial solo se pidan los datos estrictamente necesarios para pintar la primera vista útil, como mínimo:
- resumen del servidor activo
- leaderboard de la pestaña activa
- partidas recientes del servidor activo
4. Evitar que en la carga inicial se precarguen automáticamente todas las métricas del leaderboard si no son visibles todavía.
5. Eliminar o reducir estrategias agresivas de prefetch por `hover`, `focus` o cambios ligeros de interacción si están disparando peticiones innecesarias.
6. Cambiar la hidratación para que los bloques visibles se pinten de forma progresiva e independiente, sin esperar a que todo el conjunto termine si un bloque ya está listo.
7. Mantener estados de loading por bloque, pero evitar que toda la página parezca “bloqueada” por el snapshot más lento.
8. Revisar y ajustar el caché frontend para que ayude a la velocidad sin mantener estados vacíos obsoletos demasiado tiempo.
9. No cambiar la semántica funcional de los datos históricos, solo mejorar la estrategia de carga y renderizado.
10. Al completar la implementación:
- dejar el repositorio consistente
- hacer commit
- hacer push al remoto si el entorno lo permite
## Files to Read First
- AGENTS.md
- frontend/historico.html
- frontend/assets/js/historico.js
- frontend/assets/css/historico.css
- backend/app/routes.py
- backend/app/payloads.py
- backend/README.md
## Expected Files to Modify
- frontend/assets/js/historico.js
- frontend/historico.html
- frontend/assets/css/historico.css
- opcionalmente documentación mínima si el comportamiento visible cambia de forma relevante
## Constraints
- No crear páginas nuevas.
- No romper la UI histórica existente.
- No introducir frameworks nuevos.
- No hacer cambios destructivos.
- Mantener el trabajo centrado en rendimiento percibido y estrategia de carga.
## Validation
- La carga inicial hace menos peticiones que antes.
- La página empieza a mostrar contenido útil antes.
- Resumen, leaderboard activo y partidas recientes se hidratan de forma progresiva.
- No se precargan de forma agresiva métricas no visibles en el primer paint.
- Los cambios quedan committeados y se hace push si el entorno lo permite.
## Change Budget
- Preferir menos de 4 archivos modificados.
- Preferir menos de 220 líneas cambiadas.
## Outcome
- La carga inicial de `historico.html` ya no dispara precarga agresiva de todos los snapshots ni peticiones por `hover` o `focus` sobre el selector de servidor.
- La UI reutiliza snapshots cacheados de resumen, ranking activo y partidas recientes cuando existen, reduciendo la espera percibida en entradas posteriores y cambios de servidor.
- Resumen, leaderboard activo y partidas recientes se hidratan ahora de forma independiente; el bloque que llega antes se pinta antes, sin esperar al snapshot más lento.
- Validación local:
- `node --check frontend/assets/js/historico.js`
- revisión de `git diff --name-only` para confirmar alcance limitado a `frontend/assets/js/historico.js` y el archivo de task

View File

@@ -116,21 +116,11 @@ document.addEventListener("DOMContentLoaded", () => {
`${backendBaseUrl}/api/historical/snapshots/weekly-leaderboard?server=${encodeURIComponent(serverSlug)}&metric=${encodeURIComponent(metricKey)}&limit=10`,
);
const prefetchLeaderboardSnapshots = (serverSlug) => {
LEADERBOARD_METRICS.forEach((metric) => {
void getLeaderboardSnapshot(serverSlug, metric.key).catch(() => {});
});
};
const prefetchServerSnapshots = (serverSlug) => {
void getSummarySnapshot(serverSlug).catch(() => {});
void getRecentMatchesSnapshot(serverSlug).catch(() => {});
prefetchLeaderboardSnapshots(serverSlug);
};
const refreshServerContent = async () => {
const requestId = activeServerRequestId + 1;
const leaderboardRequestId = activeLeaderboardRequestId + 1;
activeServerRequestId = requestId;
activeLeaderboardRequestId = leaderboardRequestId;
const activeMetricConfig = getLeaderboardMetricConfig(activeLeaderboardMetric);
const activeServerLabel = getHistoricalServerLabel(activeServerSlug);
@@ -153,6 +143,20 @@ document.addEventListener("DOMContentLoaded", () => {
setState(recentStateNode, "Cargando partidas recientes...");
setSnapshotMeta(recentSnapshotMetaNode, "Cargando snapshot de partidas...");
const cachedSummaryPayload = readCachedPayload(
summaryCache,
buildSummarySnapshotKey(activeServerSlug),
);
if (cachedSummaryPayload) {
hydrateSummary(
{ status: "fulfilled", value: cachedSummaryPayload },
summaryNode,
rangeNode,
summaryNoteNode,
summarySnapshotMetaNode,
);
}
const cachedLeaderboardPayload = readCachedPayload(
leaderboardCache,
buildLeaderboardSnapshotKey(activeServerSlug, activeLeaderboardMetric),
@@ -174,49 +178,74 @@ document.addEventListener("DOMContentLoaded", () => {
weeklyTableNode.hidden = true;
}
const targetServerSlug = activeServerSlug;
const targetMetric = activeLeaderboardMetric;
const [summaryResult, recentMatchesResult, leaderboardResult] =
await Promise.allSettled([
getSummarySnapshot(targetServerSlug),
getRecentMatchesSnapshot(targetServerSlug),
getLeaderboardSnapshot(targetServerSlug, targetMetric),
]);
if (
requestId !== activeServerRequestId ||
targetServerSlug !== activeServerSlug ||
targetMetric !== activeLeaderboardMetric
) {
return;
const cachedRecentMatchesPayload = readCachedPayload(
recentMatchesCache,
buildRecentMatchesSnapshotKey(activeServerSlug),
);
if (cachedRecentMatchesPayload) {
hydrateRecentMatches(
{ status: "fulfilled", value: cachedRecentMatchesPayload },
recentStateNode,
recentListNode,
recentSnapshotMetaNode,
);
}
hydrateSummary(
summaryResult,
summaryNode,
rangeNode,
summaryNoteNode,
summarySnapshotMetaNode,
);
hydrateRecentMatches(
recentMatchesResult,
recentStateNode,
recentListNode,
recentSnapshotMetaNode,
);
hydrateWeeklyLeaderboard(
leaderboardResult,
weeklyStateNode,
weeklyTableNode,
weeklyBodyNode,
weeklyTitleNode,
weeklyValueHeadingNode,
weeklyWindowNoteNode,
weeklySnapshotMetaNode,
activeMetricConfig,
);
const targetServerSlug = activeServerSlug;
const targetMetric = activeLeaderboardMetric;
void settlePromise(getSummarySnapshot(targetServerSlug)).then((summaryResult) => {
if (!isActiveServerRequest(requestId, targetServerSlug, targetMetric)) {
return;
}
prefetchServerSnapshots(targetServerSlug);
hydrateSummary(
summaryResult,
summaryNode,
rangeNode,
summaryNoteNode,
summarySnapshotMetaNode,
);
});
void settlePromise(getRecentMatchesSnapshot(targetServerSlug)).then((recentMatchesResult) => {
if (!isActiveServerRequest(requestId, targetServerSlug, targetMetric)) {
return;
}
hydrateRecentMatches(
recentMatchesResult,
recentStateNode,
recentListNode,
recentSnapshotMetaNode,
);
});
void settlePromise(
getLeaderboardSnapshot(targetServerSlug, targetMetric),
).then((leaderboardResult) => {
if (
!isActiveLeaderboardRequest(
requestId,
leaderboardRequestId,
targetServerSlug,
targetMetric,
)
) {
return;
}
hydrateWeeklyLeaderboard(
leaderboardResult,
weeklyStateNode,
weeklyTableNode,
weeklyBodyNode,
weeklyTitleNode,
weeklyValueHeadingNode,
weeklyWindowNoteNode,
weeklySnapshotMetaNode,
activeMetricConfig,
);
});
};
const refreshLeaderboardContent = async () => {
@@ -284,14 +313,6 @@ document.addEventListener("DOMContentLoaded", () => {
};
selectorButtons.forEach((button) => {
button.addEventListener("mouseenter", () => {
const nextServerSlug = normalizeServerSlug(button.dataset.serverSlug);
prefetchServerSnapshots(nextServerSlug);
});
button.addEventListener("focus", () => {
const nextServerSlug = normalizeServerSlug(button.dataset.serverSlug);
prefetchServerSnapshots(nextServerSlug);
});
button.addEventListener("click", () => {
const nextServerSlug = normalizeServerSlug(button.dataset.serverSlug);
if (nextServerSlug === activeServerSlug) {
@@ -321,10 +342,29 @@ document.addEventListener("DOMContentLoaded", () => {
});
});
prefetchServerSnapshots(activeServerSlug);
void refreshServerContent();
});
function isActiveServerRequest(requestId, serverSlug, metricKey) {
return (
requestId === activeServerRequestId &&
serverSlug === activeServerSlug &&
metricKey === activeLeaderboardMetric
);
}
function isActiveLeaderboardRequest(
serverRequestId,
leaderboardRequestId,
serverSlug,
metricKey,
) {
return (
isActiveServerRequest(serverRequestId, serverSlug, metricKey) &&
leaderboardRequestId === activeLeaderboardRequestId
);
}
function hydrateSummary(result, summaryNode, rangeNode, noteNode, snapshotMetaNode) {
if (result.status !== "fulfilled") {
renderSummaryError(summaryNode);