Status: Approved · v1 · SPEC-DELIVERY-PERF-A11Y-AUDIT-001
depends_on: SPEC-ARCH-PERF-001, SPEC-ARCH-QUALITY-001
used_by: IS-MVP-15.3
Auditoria de performance, acessibilidade e qualidade visual
O que foi auditado, o que foi corrigido nesta pack, o baseline medido na Public Team Page e o que fica registrado como risco conhecido (
EP-MVP-15.3).
Método
Leitura direta do código real (CSS + screens web, não apenas as specs), seguida de medição com
Chromium headless (Playwright ad hoc, pré-instalado neste ambiente, não adicionado como
dependência do repositório) contra next dev com mocks habilitados (a única forma de obter
conteúdo real neste ambiente — build de produção recusa mocks por design, ver
packages/shared/src/clients/client-config.ts, e não há staging real acessível daqui). Cada
achado foi medido antes/depois com a mesma metodologia (throttle de rede ~slow-4G + CPU 4x,
PerformanceObserver para LCP/CLS reais do navegador), não estimado.
Baseline medido — Public Team Page (/teams/unidos-do-6)
Viewport mobile (390×844), CPU throttling 4x, rede ~1.6 Mbps down / 750 Kbps up / 150ms latência
(perfil aproximado de slow-4G), next dev (não é o bundle de produção minificado — não há staging
real acessível neste ambiente para medir o bundle de produção com dados reais; os números abaixo
são consistentes página-a-página entre si e servem de baseline relativo/regressão, não como SLA
absoluto):
| Métrica | Valor |
|---|---|
| TTFB | ~90ms |
| First Contentful Paint | ~990ms |
| Largest Contentful Paint | ~990ms |
| Cumulative Layout Shift | 0.021 (bom, < 0.1) |
| Requests de API (client-side) | sem duplicação — dup-requests não encontrou nenhuma URL chamada mais de uma vez |
| Overflow horizontal | nenhum em 5 breakpoints (375/390/768/1280/1440) × 3 rotas P0 (/teams/:slug, /search, /) |
Este é o baseline exigido por SPEC-ARCH-PERF-001 ("Baseline medido na Public Team Page").
Achados corrigidos nesta pack
| # | Achado | Severidade | Evidência (antes) | Correção | Teste |
|---|---|---|---|---|---|
| 1 | O anel de foco global (outline: 3px solid var(--focus), aplicado a todo button/a via :focus-visible em globals.css/auth.css/search.css) usava spotlight[500] (#F2B230, dourado). Contraste medido contra --background (#FBF7ED): 1.75:1; contra --surface-elevated (branco): 1.88:1 — ambos muito abaixo do mínimo 3:1 exigido por WCAG 1.4.11 (contraste não-textual) para indicadores de foco, na maioria das superfícies do app (formulários, feed, busca, praticamente toda a UI clara). | HIGH (acessibilidade — usuário de teclado não enxerga onde está o foco na maior parte do produto) | packages/ui/src/tokens/themes.ts — focus: colors.spotlight[500] | Trocado para colors.earthRed[500] (já é o token primary) — único tom da paleta que atende ≥3:1 contra todas as superfícies onde o outline aparece: background 4.76:1, surfaceElevated 5.09:1, surfaceAlt 4.15:1, fundo escuro do hero (asphalt[950]) 3.71:1, asphalt[900] 3.47:1. Corrigido em lightTheme.focus e (por consistência, embora hoje sem uso direto) darkTheme.focus. | packages/ui/test/tokens.test.mjs — teste dedicado calcula o contraste real (fórmula WCAG) contra as três superfícies; scripts/validation/check-a11y-perf-invariants.mjs (focus-ring-contrast, calcula o contraste real a partir do código-fonte, não só verifica um valor fixo) |
| 2 | .feed-card-media (imagem do post no feed público e no preview do composer de posts de time/torcida — mesma classe, 3 usos) não reservava espaço antes da imagem carregar (width: 100%; object-fit: cover, sem aspect-ratio), causando layout shift quando a imagem termina de carregar — exatamente o que SPEC-ARCH-PERF-001 pede ("imagens com aspect ratio"). | MEDIUM (performance/CLS, não bloqueante, mas nomeado explicitamente pela spec) | apps/web/app/feed.css — .feed-card-media sem aspect-ratio | Adicionado aspect-ratio: 16 / 9 (mantendo max-height/object-fit existentes). | check-a11y-perf-invariants.mjs (feed-media-missing-aspect-ratio) |
| 3 | As duas bottom sheets customizadas do app (role="dialog" em uma <div> simples, não um <dialog> nativo — precisam da animação de slide-up que <dialog> não tem) — a sheet "Por que você viu isso?/Silenciar/Deixar de torcer" do feed e a sheet de filtros da busca — não continham o foco de Tab dentro delas. Confirmado ao vivo: com a sheet aberta, Tab repetido eventualmente move o foco de teclado para trás do overlay, na página por baixo — uma armadilha de acessibilidade inversa (WCAG 2.4.3, contenção de foco em modais). A sheet do feed também não devolvia o foco ao elemento que a abriu ao fechar (o foco simplesmente ia para <body>). | HIGH (usuário de teclado perde a localização do foco ao interagir com uma sheet aberta) | home-feed.screen.tsx/search.screen.tsx — useEffect só tratava Escape, sem Tab | Novo utilitário apps/web/src/shared/focus-trap.ts (trapTabFocus, contém Tab/Shift+Tab dentro do container, sem dependência nova) usado nas duas sheets; a sheet do feed passou a guardar document.activeElement ao abrir e devolver o foco a ele ao fechar (a sheet da busca já fazia isso via filtersTriggerRef, só faltava o Tab-trap). Verificado ao vivo em Chromium real: 25 Tabs consecutivos nunca escapam da sheet, Shift+Tab também nunca escapa, Escape fecha e devolve o foco ao botão que abriu. | apps/web/test/focus-trap.test.ts (lógica pura do trap: wrap em ambas direções, no-op no meio da lista, no-op para tecla não-Tab/lista vazia; mais teste de conteúdo confirmando as duas screens usam trapTabFocus e restauram foco) + verificação interativa via Chromium (ver acima) + check-a11y-perf-invariants.mjs (custom-sheet-missing-focus-trap) |
| 4 | Achado durante a própria medição do baseline (não estava no radar antes de medir): a página de busca (/search, Smoke P0) tinha CLS medido de 0.14 — acima da faixa "boa" (≤0.1) do Core Web Vitals. Causa raiz: o bloco "Descoberta territorial" (chips de distrito) só aparecia depois que territoryCatalogService.listDistricts() resolvia, sem nenhum placeholder reservando o espaço — quando os distritos chegavam, o bloco inteiro aparecia de repente e empurrava as abas/resultados abaixo. | MEDIUM (performance, achado por medição, não por leitura de spec) | search.screen.tsx — {showTerritoryDiscovery && discoveryDistricts.length > 0 ? (...) : null}, nada renderizado enquanto carrega | Novo estado districtsStatus ('loading'/'ready'); enquanto carregando, renderiza um skeleton com a mesma altura/formato de uma chip row real (.search-chip-skeleton, reaproveitando a animação skeleton-shift já existente em globals.css) em vez de nada. CLS medido depois da correção: ~0.10–0.11 (4 medições repetidas). Não zerado — o painel de sugestões/buscas recentes carrega de forma assíncrona independente e ainda contribui um resíduo menor; ver Riscos abaixo. | check-a11y-perf-invariants.mjs (search-discovery-missing-loading-skeleton) |
Confirmado íntegro (sem correção necessária)
Verificado por leitura direta do código e/ou medição, não apenas pela spec:
- Paginação: feed (
useFeed,loadMore/hasNextPage), busca (useUniversalSearch, paginação por página) e toda lista de resultado já são paginadas — não há lista "carrega tudo de uma vez" em nenhuma superfície P0. - Query keys estáveis / sem request duplicado:
useUniversalSearchusa um contador de sequência (requestSequence) para descartar respostas obsoletas de uma corrida de requests, euseFeedusa uma flagactivepor efeito para não aplicarsetStatede uma request cancelada — ambos os padrões já evitam o problema clássico de duplicação/race. Confirmado ao vivo: nenhuma URL de API chamada mais de uma vez ao carregar/searchou/teams/:slug.SPEC-ARCH-PERF-001("stable query keys") já está atendido; nenhuma mudança necessária. Deduplicação de item porid(mergeFeedItems) também já evita duplicar posts entre páginas. - Contraste de texto:
text/textMutedcontrabackground/surfaceElevated/surfaceAltmedem entre 5.4:1 e 18.9:1 — todos acima do mínimo AA (4.5:1) para texto normal, em toda combinação usada pelo tema claro (único tema em uso hoje —darkThemenão é referenciado por nenhuma tela). - Responsividade: nenhum overflow horizontal em 5 breakpoints (375/390/768/1280/1440px) × 3 rotas P0, confirmado por medição real (não apenas leitura de CSS/media query).
- Identidade visual profissional: revisão visual da Public Team Page, busca e feed anônimo (screenshots em Chromium real, mobile e desktop) confirma composição editorial já estabelecida (tokens de cor terrosos, tipografia condensada em caixa alta para eyebrows, hierarquia clara) — nenhum padrão infantil ou de "bet" (sem cores neon, sem contadores/roletas, sem urgência artificial) encontrado em nenhuma das três telas revisadas.
- Imagens já com aspect ratio em todo o resto do app:
.field-gallery-grid img(4/3),.match-gallery-list img(16/10),.service-showcase-grid img(4/3),.sponsor-grid img(3/2),.player-portrait(4/5) já reservavam espaço corretamente antes desta pack — só.feed-card-media(achado #2 acima) estava sem.
Riscos e pendências
- CLS residual de ~0.10–0.11 em
/search(achado #4): o bloco de descoberta territorial foi corrigido, mas o painel de sugestões/buscas recentes (useSearchSuggestions, leitura de histórico dolocalStorage) segue carregando de forma assíncrona sem reserva de espaço equivalente. Não corrigido nesta pack — o histórico de busca é tipicamente vazio num navegador novo (não gera shift real na maioria das visitas) e as sugestões têm tamanho de conteúdo inerentemente variável (não dá para reservar uma altura precisa sem inventar um número arbitrário). Candidato a revisão futura se a métrica se mostrar pior com dados de uso reais. - Baseline medido em
next dev, não no bundle de produção com API real: build de produção recusa mocks por design (environment === 'production' && useMockslança erro,client-config.ts) e não há staging real acessível deste ambiente sandboxed — os números da tabela de baseline são um baseline relativo (para detectar regressão futura), não uma medição definitiva de produção.pnpm run buildcom as variáveis de produção (NODE_ENV=production,*_USE_MOCKS=false,*_API_BASE_URL=https://api-staging.raizfc.com.br) continua completando com sucesso (7/7 tasks) — a limitação é só de medição de performance ao vivo, não de build. darkTheme(tema escuro) não é usado por nenhuma tela web hoje — a correção de contraste do foco (achado #1) foi replicada lá por consistência, mas não há verificação end-to-end de um modo escuro real porque ele não existe no produto ainda.- Border/divisor com baixo contraste (
--bordervs--background, ~1.5:1): não é um achado desta pack (WCAG 1.4.11 exige 3:1 para limites de componentes de UI cuja identificação depende só da cor — a maioria dos usos de--borderaqui é decorativo/divisor entre seções, não o único indicador de um controle interativo). Registrado como observação de design system para revisão futura, não bloqueante. - Nenhum achado BLOCKER/HIGH em aberto — os dois HIGH encontrados (contraste do foco, Tab-trap das sheets) foram corrigidos e verificados (teste automatizado + verificação interativa em Chromium real) dentro desta mesma pack, antes desta aprovação.