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`,
|
||||
);
|
||||
|
||||
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,20 +178,23 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
weeklyTableNode.hidden = true;
|
||||
}
|
||||
|
||||
const cachedRecentMatchesPayload = readCachedPayload(
|
||||
recentMatchesCache,
|
||||
buildRecentMatchesSnapshotKey(activeServerSlug),
|
||||
);
|
||||
if (cachedRecentMatchesPayload) {
|
||||
hydrateRecentMatches(
|
||||
{ status: "fulfilled", value: cachedRecentMatchesPayload },
|
||||
recentStateNode,
|
||||
recentListNode,
|
||||
recentSnapshotMetaNode,
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
) {
|
||||
void settlePromise(getSummarySnapshot(targetServerSlug)).then((summaryResult) => {
|
||||
if (!isActiveServerRequest(requestId, targetServerSlug, targetMetric)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -198,12 +205,35 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
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,
|
||||
@@ -215,8 +245,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
weeklySnapshotMetaNode,
|
||||
activeMetricConfig,
|
||||
);
|
||||
|
||||
prefetchServerSnapshots(targetServerSlug);
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user