01 — Resultados Coordenador: causa-raiz do mount lento
Ref: 00_INDEX · Frentes geradas: F1, F2, F4
Contexto
Sintoma reportado pelo usuário: ~6s de skeleton no mount da tela Resultados; ~2s adicionais ao abrir uma olimpíada; piora a cada troca de página da tabela; UI lenta também durante recompute/save de faixas.
Evidência
E1.1 — Quantas actions o mount dispara
src/hooks/useGestaoResultados.ts expõe estes hooks consumidos pela tela:
| Hook | Action backend | LoC ref |
|---|---|---|
useResultadosInit | batch_init | 128 |
useResultadosPorOlimpiada | list_by_olimpiada (modo full) | 178 |
useResultadosPorOlimpiadaPaginado | list_by_olimpiada (modo paginado) | 250 |
useEstatisticasResultados | stats | 291 |
useEstatisticasAgregadas | stats_agregados | 311 |
useConfigsFase | get_configs_fase | 331 |
resultados-lista.tsx:37 usa apenas useResultadosInit (que internamente chama batch_init) → uma única chamada no mount da tela "lista de olimpíadas". OK.
resultados-detalhes.tsx:34 usa useResultadosPorOlimpiadaPaginado para a tabela + useResultadosPorOlimpiada (legacy) só quando o modal de premiação manual abre (lazy via enabled).
→ A lista raiz é 1 request. O detalhe é 1 request. Sem fan-out na UI. O fan-out está dentro do backend.
E1.2 — O que o backend faz em cada action
supabase/functions/_shared/resultados-queries.ts:
handleBatchInit(linha 759) → drenainscricoes_olimpiadada escola inteira viafetchAllRows(linha 791) + computa stats em JS.handleListByOlimpiada(linha 191) → drena 100% das inscrições da olimpíada (linha 313) mesmo quando paginado, depois pagina/filtra/ordena em JS (linhas 470–500).handleStats(linha 704) → drenainscricoes(linha 715) para contar.handleStatsAgregados(linha 549) → drenainscricoes(linha 567) para agregar por olimpíada.
8 chamadas a fetchAllRows em resultados-queries.ts (grep confirma: linhas 41, 146, 237, 313, 567, 715, 791, 1017, 1063). Cada uma faz no mínimo 2 round-trips DB (página 1: 1000 linhas, página 2: 1000 linhas) para uma escola com 1.4k inscrições.
E1.3 — O JOIN puxado no payload
handleListByOlimpiada linhas 318-339: select carrega para CADA inscrição:
id, aluno_id, nivel_id,
alunos:aluno_id ( id, nome_completo, matricula, turma_id,
turmas:turma_id ( id, identificador, serie_id,
series_escolares:serie_id ( id, label ) ) ),
niveis_competicao:nivel_id ( id, titulo )Para 2k inscrições isso é ~2k objetos aninhados de 4 níveis. Estimativa de payload bruto: 0.8–1.2 MB de JSON em uma única Response — Edge runtime tem que parsear, serializar e o navegador idem. Cada página repete o trabalho inteiro (a paginação acontece DEPOIS desse fetch).
E1.4 — Por que piora a cada página
useResultadosPorOlimpiadaPaginado (linha 250) tem queryKey com page no array (linha 252) → cada mudança de página é uma nova queryKey → nova chamada ao backend → backend re-drena 100% das inscrições e fatia. Sem staleTime longo nem reuso. keepPreviousData não foi confirmado (precisa olhar CACHE_CONFIG).
E1.5 — Skeleton "estranho cinza" de 6s
resultados-lista.tsx:253-613 repete if (isLoading || filtroAno === null) em 4 lugares — todos atrelados ao MESMO isLoadingBatch do useResultadosInit. Logo, o skeleton só sai quando batch_init retornar. Se batch_init está drenando inscrições da escola toda, toda a tela trava esperando a query mais cara.
Hipóteses
P01 — Causa principal. batch_init é caro porque drena tabela escalável para computar stats que poderiam vir de SQL agregado (SELECT COUNT(*), nivel_id, situacao FROM ... GROUP BY ...). Custo cresce O(N inscrições) por mount.
P02 — Causa principal. list_by_olimpiada faz paginação client-side em cima de drenagem completa. Custo cresce O(N) por toda troca de página.
P03 — Amplificadora. O JOIN aninhado (E1.3) infla cada linha. Mesmo com paginação real no servidor (range), trazer turma+serie+nivel embed multiplica bytes. Lookup map separado seria mais barato.
P04 — Amplificadora UX. Skeleton único bloqueia a tela inteira esperando a query mais cara. Renderizar olimpíadas assim que list chegar (separado de stats) melhora TTI percebido — mesmo sem reduzir tempo de servidor.
P05 — Amplificadora. CACHE_CONFIG aplicado a todas as queries (precisa confirmar valores) — sem staleTime adequado nem keepPreviousData, troca de página = flash de skeleton.
O que NÃO é a causa
- NÃO é o RLS. Queries têm filtro
escola_idexplícito alinhado com policies. - NÃO é falta de índice.
inscricoes_olimpiada(escola_id, olimpiada_id, ano_edicao)é exercido pelas queries. (Validar comEXPLAINna Onda A.) - NÃO é StrictMode (mount duplo) — sintoma é consistente em 1 render, e o impacto é dominado pelo backend.
- NÃO é Preview vs Prod. Preview agrava (~30%), mas a estrutura do problema é a mesma — confirmado pelo padrão de payload e número de queries.
Próximas etapas
→ Detalhamento técnico do tradeoff fetchAllRows em 02_PAGINATION_FETCHALL_TRADEOFF.md → Padrão de referência (Importação Alunos) em 04_ALUNOS_ESCOLA_BENCHMARK.md → Frentes F1 (paginação servidor), F2 (SQL agregado), F4 (batch_init consolidado) endereçam P01–P05.