overfetch: auditoria de requisições
Você está produzindo um relatório no qual alguém agirá: uma lista ranqueada das maneiras pelas quais este aplicativo faz mais requisições do que suas telas precisam, cada uma com evidências, uma correção e o número de requisições que deve mover. O relatório só é útil se cada descoberta sobreviver a um leitor cético, então o método abaixo gasta a maior parte do seu esforço em verificação e em dizer claramente o que foi e o que não foi verificado.
O que você entrega
Um relatório em Markdown (template em references/report-template.md) com:
- Escopo e evidências: qual árvore, qual commit, quais capturas.
- Cobertura: cada rota, chunk lazy e site de dados protegido, e quais deles foram lidos apenas no código versus exercitados em uma captura.
- Descobertas ranqueadas por como o número de requisições cresce, cada uma com
file:lineou evidência de captura, causa raiz, padrão de correção, delta esperado, esforço. - Não-descobertas: padrões que você verificou e descartou, com a razão. Isso impede que a próxima pessoa reinvestigue.
- Decisões de produto abertas e pedidos ao backend: coisas que precisam de um humano ou de uma mudança de API, não de um patch no cliente.
- Limites: o que a auditoria não conseguiu medir.
Ofereça-se para implementar as correções depois, um PR por descoberta, maior gravidade primeiro. Não comece a corrigir durante a auditoria.
Princípios
Um hit de regex é um candidato, não uma descoberta. scripts/static_scan.py lê texto. Ele não consegue distinguir uma barra de abas de 3 linhas de uma tabela de 3.000 linhas, ou uma invalidação raiz deliberada após uma importação em massa de uma preguiçosa. Leia o código por trás de cada candidato que pretende relatar. Diga o que leu.
O medido vence o alegado. Uma captura do aplicativo em execução é a verdade sobre contagens de requisições; o código é a verdade sobre o porquê. Comentários, documentação e registros de auditoria antigos são alegações. Quando um documento diz "fresco por 60 segundos" e o código diz staleTime: Infinity, relate o código, anote a contradição e registre o hash do commit que leu.
Conheça a semântica real da biblioteca antes de culpá-la. Os erros que custam mais são afirmações confiantes que estão erradas: "o cache deduplica isso" quando as chaves diferem, "o polling pausa quando ocioso" quando ele só pausa quando oculto, "três chamadores, três buscas por minuto" quando a biblioteca ressincroniza temporizadores de mesma chave e compartilha a busca. references/library-facts.md contém os fatos para TanStack Query, SWR, Apollo e RTK Query, com o caminho de origem que foi verificado. Leia a seção para a biblioteca em uso antes de escrever qualquer frase sobre cache ou polling, e quando uma afirmação decidir uma descoberta, abra o código-fonte da biblioteca instalada e confirme-a.
Classifique por crescimento, não por contagem. Uma leitura que escala com linhas, com componentes montados ou com o tempo é pior do que uma duplicata fixa. A rubrica de gravidade abaixo é sobre classe de crescimento.
Cobertura tem um denominador. Uma captura vê apenas o código que carregou enquanto executava: as rotas que alguém visitou, os diálogos e abas que alguém abriu, as flags que estavam ativas, os estados que o backend retornou. Chunks lazy, telas protegidas e caminhos de erro são exatamente onde os bugs de requisição se escondem, porque ninguém olha para eles durante o desenvolvimento. Portanto, toda auditoria começa a partir da superfície completa (coverage_map.py), cada "sem problema" é limitado ao que foi coberto, e o relatório diz quais partes foram exercitadas ao vivo e quais foram apenas lidas.
Diga o que está bem. Uma pré-busca de hover que é uma leitura deduplicada por linha é uma não-descoberta. Escreva-a como uma. Um relatório que lista apenas problemas faz o leitor se perguntar o que mais foi perdido.
Números vêm de algum lugar. Cada contagem no relatório remete a um arquivo de captura ou a um caminho de código. Não invente "6+" ou "aproximadamente 100" por intuição; se não puder medir, diga "não medido" e dê a fórmula (linhas × leituras por linha).
Fluxo de trabalho
Informe ao usuário com antecedência quanto tempo isso leva. Uma auditoria apenas estática de um aplicativo de médio porte leva de 15 a 30 minutos de leitura. Adicionar capturas ao vivo adiciona de 15 a 45 minutos, dependendo de quantos jornadas importam.
1. Escopo
- Encontre a(s) raiz(es) do app e a biblioteca de dados a partir de
package.json(a varredura imprime ambas). Monorepos: escaneie cada app separadamente. - Encontre a camada de dados: fábricas de chaves de consulta, módulos de transporte/serviço, a construção
QueryClient(ou configuração SWR, cliente Apollo,createApido RTK). Leia os padrões primeiro; eles definem a linha de base contra a qual cada descoberta é medida. - Liste as telas que importam: geralmente a rota de destino após o login, a lista mais movimentada e qualquer tela deixada aberta por horas (um shell, um monitor). Pergunte ao usuário se não for óbvio; caso contrário, escolha essas três e diga isso.
- Registre o commit:
git rev-parse HEAD. Cada linha do relatório se refere a esta revisão. - Decida se uma captura ao vivo é possível: um servidor de desenvolvimento em execução mais um navegador que você possa controlar, ou um HAR que o usuário já possui. Se nenhum, a auditoria é apenas estática, e o relatório deve dizer isso em seu primeiro parágrafo.
2. Mapa de cobertura
python3 <skill-dir>/scripts/coverage_map.py <app-root> --format md --out coverage.md
Este é o denominador. Ele lista cada rota (roteadores baseados em arquivo para TanStack Router, Next.js, Remix, SvelteKit e SolidStart; roteadores definidos por código por seus literais de caminho; arquivos de entrada caso contrário), cada limite dinâmico import() com as leituras por trás dele, cada módulo de dados protegido por uma flag, plataforma, permissão ou verificação de ambiente, cada literal de caminho de endpoint no código-fonte e os módulos de dados que nenhuma rota alcança. Ele é executado em alguns segundos em um aplicativo de 2.000 arquivos.
Use-o de três maneiras:
- Para planejar. O plano de captura na parte inferior nomeia as rotas com mais locais de leitura, os chunks lazy a acionar e as proteções a alternar. Use-o para escolher os jornadas na etapa 6 em vez de adivinhar.
- Para definir o escopo da varredura. Uma descoberta em um módulo que nenhuma rota alcança é código morto ou alcançado por algo que o mapa não consegue resolver. Verifique qual antes de chamá-lo de ativo.
- Para fechar o ciclo. Após a captura, execute-o novamente com as capturas; ele marca quais rotas foram visitadas, quais módulos e chunks lazy carregados (exatamente, para uma captura de servidor de desenvolvimento), quais módulos protegidos foram executados e quais caminhos de endpoint apareceram:
python3 <skill-dir>/scripts/coverage_map.py <app-root> --har home.har --har library.har --visited home.har.visited.txt --format md
Repita as capturas até que as rotas e chunks lazy com mais locais de leitura sejam cobertos, ou escreva por que não são (sem dados de fixture, precisa de uma função que você não tem, plataforma que não pode executar).
3. Varredura estática
python3 <skill-dir>/scripts/static_scan.py <app-root> --format md --out scan.md
python3 <skill-dir>/scripts/static_scan.py <app-root> --format json --out scan.json
A varredura é rápida (cerca de dez segundos para 2.000 arquivos) e precisa apenas de Python 3.9+. Ela emite candidatos com uma gravidade, uma confiança, o detector que disparou, file:line, por que importa, o padrão de correção e uma instrução "verificar primeiro". Leia scan.md por inteiro; o JSON é para script e para o harness de avaliação.
Adicione --exclude para árvores geradas que os padrões perdem, --min-confidence medium quando a lista for longa e você quiser a primeira passagem mais enxuta.
4. Verificar candidatos
Trabalhe através dos candidatos nesta ordem: P0 por confiança, depois P1, depois o resto. Para cada um que você mantiver, você deve ter aberto o arquivo e ser capaz de responder:
- Qual é o multiplicador por linha ou por observador, e o que o limita? (Um
.mapsobre um enum fixo não é N+1. Uma contagem de linhas de uma lista paginada é.) - O cache já o absorve? (Mesma chave, entrada quente, leituras com
enabled: false, um pai que já buscou o objeto.) - Existe um endpoint de lote ou projeção que o cliente poderia usar? Leia o cliente de API ou a especificação OpenAPI. Se nenhum existir, a correção é um pedido ao backend, e a descoberta fica com "bloqueado pelo backend" nela.
- Qual é o padrão de correção, pelo nome, de
references/catalog.md?
Descarte candidatos que falhem nessas verificações e mantenha aqueles que você descartou e que um leitor provavelmente levantaria: eles vão para a tabela de não-descobertas com uma razão de uma linha.
A entrada do catálogo para cada detector lista as condições específicas de não-descoberta. Leia-a para cada id de detector que aparecer na varredura; ela está organizada pelos mesmos ids.
5. Verificações transversais que o scanner não consegue fazer
Elas precisam de leitura, não de grep. Faça todas; a maioria das auditorias encontra pelo menos uma.
- Canonicidade de chave. Escolha os cinco endpoints mais lidos e encontre cada chave de consulta que lê cada um. Duas chaves para uma leitura são duas entradas de cache e duas buscas. Observe especialmente leituras de lista sob diferentes projeções (completa vs apenas nomes) e diferenças de escopo (org vs workspace) que não são realmente dados diferentes.
- Leituras em voo compartilhadas. Dois componentes montados no mesmo quadro com a mesma chave compartilham uma requisição apenas se a chave for idêntica naquele momento. Chaves construídas a partir de um estado que se estabiliza um tick depois (um id que chega de outra consulta) produzem duas buscas: uma com
undefined, uma real. Verifique as proteçõesenabledem chaves dependentes. - Frescor por superfície. Faça uma tabela de staleTime por família de dados (identidade, listas, detalhes, status ao vivo) e compare com a rapidez com que cada um realmente muda. O bug usual é um valor global que não é certo para nada.
- Propriedade de polling. Para cada opção de polling em um hook compartilhado ou fábrica de opções, liste os componentes que podem estar montados ao mesmo tempo e a chave que cada um usa. No SWR e Apollo, cada instância executa seu próprio temporizador e as buscas se multiplicam, a menos que caiam em uma janela de deduplicação. No TanStack Query, os temporizadores reiniciam a cada atualização de consulta e compartilham uma busca, então observadores de mesma chave custam uma busca por intervalo mais curto; o empilhamento aparece em chaves ou clientes, e a descoberta é a propriedade da cadência. No RTK Query, um temporizador por entrada de cache é executado no intervalo mais curto.
- Foco e visibilidade. O que dispara refetches de foco nesta plataforma? Um shell de desktop (Tauri, Electron) frequentemente conecta o foco da janela do SO, que dispara com muito mais frequência do que a visibilidade da aba. Quais polls pausam quando ocultos e quais continuam em execução enquanto o usuário está ocioso em uma tela visível?
- Teto de concorrência. Existe um limitador para leituras fan-out e qual é? Observe que um limitador de 6 em voo não é um limite de 6 requisições por segundo; não escreva um como o outro.
- Contrato de paginação. Para coleções de total desconhecido, o cliente para quando uma página retorna curta ou continua perguntando até uma página vazia? Verifique os tamanhos de página em relação ao que a tela pode mostrar.
- Retries. O que o cliente faz em 403/404/422? O padrão da biblioteca tenta novamente três vezes; um N+1 de 100 linhas com um chunk proibido se torna 400 requisições.
- Persistência. Se o cache for persistido (IndexedDB, localStorage, um armazenamento nativo), qual é o
maxAge, o que o quebra na implantação e o armazenamento é por plataforma? Um cache persistido muda o que "carga fria" significa em uma captura; diga qual você mediu. - Mutações. Para as cinco escritas mais comuns, o que é invalidado? Invalidações raiz após uma pequena escrita são a descoberta usual.
6. Captura ao vivo
Se você puder executar o aplicativo ou o usuário lhe deu uma captura, obtenha números. references/capture.md tem os jornadas a executar, a matriz de estados (vazio, preenchido, erro, lento, offline, ocioso, oculto, protegido, cache frio e quente), como capturar (HAR das DevTools, Playwright, o scripts/capture.mjs incluso ou o próprio log de requisições do app) e como manter as capturas comparáveis.
Escolha os jornadas do plano do mapa de cobertura, não do hábito. Prefira o servidor de desenvolvimento quando puder executá-lo: suas capturas nomeiam cada módulo que carregou, então o mapa pode dizer exatamente quais chunks lazy e módulos protegidos foram executados. Em seguida, execute o mapa novamente com as capturas (etapa 2) e capture as lacunas ou liste-as em Limites.
Uma captura mostra o que aconteceu, nunca o porquê. Em uma auditoria apenas de captura, atenue cada cláusula causal onde ela está ("consistente com duas chaves de consulta", "provavelmente uma remontagem"), não uma vez em um aviso no topo: os leitores citam frases individuais, e um "porque" não atenuado viaja sem seu aviso.
Quando tudo o que você tem é uma captura que outra pessoa fez, você não pode ver o que ela perdeu apenas pela captura. Diga isso, liste o que uma única sessão geralmente perde (rotas não visitadas, diálogos e abas não abertos, variantes de flag e função, estados de erro e vazio, comportamento de aba ociosa e oculta) e recomende as capturas que a fechariam.
python3 <skill-dir>/scripts/har_analyze.py capture.har --format md --out capture.md
Ele relata requisições de API por página, rajadas por linha em um modelo de caminho, polling com o número de temporizadores empilhados por chave, duplicatas dentro de uma janela (com hash de corpo para POSTs), cadeias seriais, erros repetidos e as maiores respostas. Ativos estáticos são excluídos; use --ignore para hosts de telemetria.
Duas coisas a manter separadas ao lê-lo:
- Um POST para a mesma URL duas vezes é uma duplicata apenas quando o hash do corpo corresponde. Se os corpos não foram capturados, a ferramenta diz "não verificado"; relate dessa forma.
- Uma cadeia serial é um waterfall apenas se a requisição posterior não precisava de nada da anterior. A ferramenta encontra a forma; você confirma a dependência no código.
7. Reconciliar
Emparelhe cada descoberta estática com evidência de captura onde uma captura cobre aquela tela. Quando discordam, a captura vence para contagens e o código vence para causa. Quando uma captura mostra amplificação que nenhum candidato explica, leia o código até que o faça; isso geralmente é um problema de canonicidade de chave ou chave dependente da etapa 4.
Se o repositório contiver uma auditoria anterior ou documento de desempenho, verifique cada uma de suas afirmações que tocam suas descobertas contra o código neste commit e liste as contradições no relatório. Alegações desatualizadas são como o mesmo bug é "corrigido" duas vezes.
8. Classificar e escrever
Use a rubrica:
| Gravidade | Classe de crescimento | Formas típicas |
|---|---|---|
| P0 | Cresce sem teto: com linhas, com observadores montados ou com o tempo | consulta por linha em uma lista paginada, temporizadores de polling por instância em um hook compartilhado (SWR, Apollo, feito à mão), invalidação a cada tecla, intervalo não limpo |
| P1 | Multiplicador fixo em um caminho quente | staleTime 0 em uma lista que toda tela lê, refetch em cada foco, busca orientada por efeito sem cache, invalidação genérica após uma única escrita |
| P2 | Multiplicador fixo em um caminho frio ou condicional | pré-busca em um modal que raramente abre, refetch em foco em uma tela que poucos veem, polling em uma rota não visitada |
| P3 | Sem amplificação de requisição, mas custo de carga útil | busca de campos que a tela não renderiza (over-fetching), formato de resposta não compactado quando uma projeção existe |
Escreva descobertas como um título e uma tabela:
### A tela de perfil busca detalhes do usuário e as contas associadas em duas requisições quando uma serviria
| | |
| --- | --- |
| **Raiz** | `src/routes/profile.tsx` (linha 25–31) busca `["user", id]` e depois `["accounts", userId]` em dois `useSuspenseQuery`; a API retorna contas aninhadas em `/users/:id`
| **Por que isso importa** | 2 requisições em vez de 1 na tela mais visitada após o login, 20 ms de latência extra em 3G
| **Correção** | Substitua por uma consulta `["user-with-accounts", id]` que use o endpoint combinado `/users/:id?include=accounts` (o backend o expõe; veja `src/api/spec.ts`); delta esperado: −50% de requisições nesta tela
| **Evidência** | `capture/profile-3g.har`: 2 GETs para esta rota; código em `src/routes/profile.tsx`
| **Esforço** | 30 min: mudar consulta, remover hook de contas, ajustar tipo |
Para não-descobertas, use a mesma forma, mas substitua a raiz por uma razão:
### Itens de lista: pré-busca de hover por linha descartada (esperada)
**Por que não é um problema:** O detector `hover-prefetch-per-row` disparou; os prefetches são deduplicados pelo TanStack Query (mesma chave `["repo", owner, name]`) e a biblioteca compartilha a busca durante a janela `staleTime` de 30 segundos do `QueryClient`. Cada linha dispara um fetch somente se a chave não estiver no cache; as linhas visíveis são pré-buscadas uma vez.
9. Fechar
- Leia o rascunho do relatório em voz alta para si mesmo. Se uma frase usar "claramente", "obviamente" ou "é evidente que", apague-a e coloque a evidência.
- Verifique cada contagem: abra o arquivo de captura ou o bloco de código e verifique se o número corresponde.
- Certifique-se de que a seção Não-descobertas seja tão longa quanto a seção Descobertas. Se não for, você perdeu um padrão que verificou.
- Pergunte: "Se eu estivesse lendo isso sobre meu próprio código, eu acreditaria?" Se a resposta for não, encontre a lacuna e preencha-a.
Referências incluídas no skill
references/catalog.md— catálogo de detectores com padrões de correção e condições de não-descobertareferences/library-facts.md— semântica de cache comprovada para TanStack Query, SWR, Apollo, RTK Queryreferences/capture.md— jornadas de captura, matriz de estados, métodos de captura, guia de comparabilidadereferences/report-template.md— modelo de relatório que você preenchescripts/coverage_map.py— gera mapa de cobertura do aplicativo, aceita HAR opcionaisscripts/static_scan.py— varredura estática para candidatos a amplificação de requisiçãoscripts/har_analyze.py— analisa HAR para contagens de requisição, rajadas, polling, duplicatas, cadeias seriaisscripts/capture.mjs— script de captura navegável baseado em Playwright
Execute qualquer script com --help para ver as opções.