Complete TASK-052 historical UI load pass
This commit is contained in:
@@ -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
|
||||||
@@ -116,21 +116,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
`${backendBaseUrl}/api/historical/snapshots/weekly-leaderboard?server=${encodeURIComponent(serverSlug)}&metric=${encodeURIComponent(metricKey)}&limit=10`,
|
`${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 refreshServerContent = async () => {
|
||||||
const requestId = activeServerRequestId + 1;
|
const requestId = activeServerRequestId + 1;
|
||||||
|
const leaderboardRequestId = activeLeaderboardRequestId + 1;
|
||||||
activeServerRequestId = requestId;
|
activeServerRequestId = requestId;
|
||||||
|
activeLeaderboardRequestId = leaderboardRequestId;
|
||||||
const activeMetricConfig = getLeaderboardMetricConfig(activeLeaderboardMetric);
|
const activeMetricConfig = getLeaderboardMetricConfig(activeLeaderboardMetric);
|
||||||
const activeServerLabel = getHistoricalServerLabel(activeServerSlug);
|
const activeServerLabel = getHistoricalServerLabel(activeServerSlug);
|
||||||
|
|
||||||
@@ -153,6 +143,20 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
setState(recentStateNode, "Cargando partidas recientes...");
|
setState(recentStateNode, "Cargando partidas recientes...");
|
||||||
setSnapshotMeta(recentSnapshotMetaNode, "Cargando snapshot de partidas...");
|
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(
|
const cachedLeaderboardPayload = readCachedPayload(
|
||||||
leaderboardCache,
|
leaderboardCache,
|
||||||
buildLeaderboardSnapshotKey(activeServerSlug, activeLeaderboardMetric),
|
buildLeaderboardSnapshotKey(activeServerSlug, activeLeaderboardMetric),
|
||||||
@@ -174,20 +178,23 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
weeklyTableNode.hidden = true;
|
weeklyTableNode.hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cachedRecentMatchesPayload = readCachedPayload(
|
||||||
|
recentMatchesCache,
|
||||||
|
buildRecentMatchesSnapshotKey(activeServerSlug),
|
||||||
|
);
|
||||||
|
if (cachedRecentMatchesPayload) {
|
||||||
|
hydrateRecentMatches(
|
||||||
|
{ status: "fulfilled", value: cachedRecentMatchesPayload },
|
||||||
|
recentStateNode,
|
||||||
|
recentListNode,
|
||||||
|
recentSnapshotMetaNode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const targetServerSlug = activeServerSlug;
|
const targetServerSlug = activeServerSlug;
|
||||||
const targetMetric = activeLeaderboardMetric;
|
const targetMetric = activeLeaderboardMetric;
|
||||||
const [summaryResult, recentMatchesResult, leaderboardResult] =
|
void settlePromise(getSummarySnapshot(targetServerSlug)).then((summaryResult) => {
|
||||||
await Promise.allSettled([
|
if (!isActiveServerRequest(requestId, targetServerSlug, targetMetric)) {
|
||||||
getSummarySnapshot(targetServerSlug),
|
|
||||||
getRecentMatchesSnapshot(targetServerSlug),
|
|
||||||
getLeaderboardSnapshot(targetServerSlug, targetMetric),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (
|
|
||||||
requestId !== activeServerRequestId ||
|
|
||||||
targetServerSlug !== activeServerSlug ||
|
|
||||||
targetMetric !== activeLeaderboardMetric
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,12 +205,35 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
summaryNoteNode,
|
summaryNoteNode,
|
||||||
summarySnapshotMetaNode,
|
summarySnapshotMetaNode,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
void settlePromise(getRecentMatchesSnapshot(targetServerSlug)).then((recentMatchesResult) => {
|
||||||
|
if (!isActiveServerRequest(requestId, targetServerSlug, targetMetric)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
hydrateRecentMatches(
|
hydrateRecentMatches(
|
||||||
recentMatchesResult,
|
recentMatchesResult,
|
||||||
recentStateNode,
|
recentStateNode,
|
||||||
recentListNode,
|
recentListNode,
|
||||||
recentSnapshotMetaNode,
|
recentSnapshotMetaNode,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
void settlePromise(
|
||||||
|
getLeaderboardSnapshot(targetServerSlug, targetMetric),
|
||||||
|
).then((leaderboardResult) => {
|
||||||
|
if (
|
||||||
|
!isActiveLeaderboardRequest(
|
||||||
|
requestId,
|
||||||
|
leaderboardRequestId,
|
||||||
|
targetServerSlug,
|
||||||
|
targetMetric,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
hydrateWeeklyLeaderboard(
|
hydrateWeeklyLeaderboard(
|
||||||
leaderboardResult,
|
leaderboardResult,
|
||||||
weeklyStateNode,
|
weeklyStateNode,
|
||||||
@@ -215,8 +245,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
weeklySnapshotMetaNode,
|
weeklySnapshotMetaNode,
|
||||||
activeMetricConfig,
|
activeMetricConfig,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
prefetchServerSnapshots(targetServerSlug);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshLeaderboardContent = async () => {
|
const refreshLeaderboardContent = async () => {
|
||||||
@@ -284,14 +313,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
selectorButtons.forEach((button) => {
|
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", () => {
|
button.addEventListener("click", () => {
|
||||||
const nextServerSlug = normalizeServerSlug(button.dataset.serverSlug);
|
const nextServerSlug = normalizeServerSlug(button.dataset.serverSlug);
|
||||||
if (nextServerSlug === activeServerSlug) {
|
if (nextServerSlug === activeServerSlug) {
|
||||||
@@ -321,10 +342,29 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
prefetchServerSnapshots(activeServerSlug);
|
|
||||||
void refreshServerContent();
|
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) {
|
function hydrateSummary(result, summaryNode, rangeNode, noteNode, snapshotMetaNode) {
|
||||||
if (result.status !== "fulfilled") {
|
if (result.status !== "fulfilled") {
|
||||||
renderSummaryError(summaryNode);
|
renderSummaryError(summaryNode);
|
||||||
|
|||||||
Reference in New Issue
Block a user