Skip to content

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:

HookAction backendLoC ref
useResultadosInitbatch_init128
useResultadosPorOlimpiadalist_by_olimpiada (modo full)178
useResultadosPorOlimpiadaPaginadolist_by_olimpiada (modo paginado)250
useEstatisticasResultadosstats291
useEstatisticasAgregadasstats_agregados311
useConfigsFaseget_configs_fase331

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) → drena inscricoes_olimpiada da escola inteira via fetchAllRows (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) → drena inscricoes (linha 715) para contar.
  • handleStatsAgregados (linha 549) → drena inscricoes (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:

ts
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_id explícito alinhado com policies.
  • NÃO é falta de índice. inscricoes_olimpiada(escola_id, olimpiada_id, ano_edicao) é exercido pelas queries. (Validar com EXPLAIN na 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.