Skip to content

perf-optimizer: corrige duas instruções e cobre o <head>/lote eager - #342

Open
viniciusventura29 wants to merge 1 commit into
mainfrom
perf/head-priority-and-hydration
Open

perf-optimizer: corrige duas instruções e cobre o <head>/lote eager#342
viniciusventura29 wants to merge 1 commit into
mainfrom
perf/head-priority-and-hydration

Conversation

@viniciusventura29

@viniciusventura29 viniciusventura29 commented Sep 3, 2026

Copy link
Copy Markdown

O catálogo do perf-optimizer cobre bem opportunity de terceiros (embed, fonte, cache header), mas não tinha nada para o caso em que o LCP é ruim com todo o resto verde — e duas das instruções que já estão lá estão medidas como armadilha.

Tudo abaixo é before/after medido numa migração real. Sem identificar o site.

Correções em linhas que já existiam

Linha O que dizia Por que muda
uses-responsive-images "Add explicit width/height" Em componente dirigido por CMS o width costuma ser também o alvo de resize passado ao CDN de imagem — escrever a largura intrínseca faz cada imagem baixar em resolução cheia. A reserva de caixa correta é aspectRatio
prioritize-lcp-image eager + fetchpriority="high" Necessário e não suficiente. Se o Load Delay continuar alto, a imagem está perdendo disputa de banda para modulepreload (High) — o browser agenda por prioridade, não por ordem de documento. Medido: hoist do preload não moveu nada, Load Delay ficou em 5239 ms
regra de 3P "Defer it to load if it isn't already" Falta o sequenciamento: TBT é medido FCP→TTI, então com LCP > 4 s o trabalho deferido continua dentro da janela. Medido: score 44 → 30. LCP/CLS primeiro

E na mesma linha de imagem, para quando for preciso cortar bytes: menos resolução a qualidade alta domina mesma resolução a qualidade baixa — por SSIM, 620w@high foi menor (57 219 B contra 60 708 B) e mais fiel (0,9412 contra 0,9340) que 824w@low.

Linhas novas no catálogo

  • unused-javascript do NOSSO lote eager. Atribuir antes de refatorar: decodificar as mappings do .js.map, nunca sourcesContent — este mede o arquivo original e superestimou um pacote em 5x (499 966 B contra 101 631 B), mandando você atrás de bytes que o bundler já removeu. Depois procurar duas coisas: módulo dev/admin alcançável por import estático (um comentário dizendo "só carrega quando…" não é gate; só import() é) e código de primeira parte que a lista de exclusão do glob "exclui" mas um import nomeado traz de volta. Não splitar por módulo: medido, o JS total quase não mudou (1,727 → 1,737 MB) e virou 27 requests a mais sem compressão de stream — score local 42 → 20.
  • unsized-images / CLS de banner de CMS. Checar o componente primeiro: se ele já aceita dimensão, o que falta é conteúdo (1192 de 1198 banners sem valor). Não chutar razão — ler a dimensão real do header do arquivo com Range: bytes=0-65535. E se o conteúdo é regerado por bot, o fix tem de ser script de post-generate: um refresh de conteúdo desfez em silêncio um fix de CLS já mergeado (0,00 → 0,95, reproduzido duas vezes).
  • dom-size. Ranquear por bytes transferidos e contagem de nós, nunca por share de bytes crus. Markup SSR repetitivo é o caso ótimo do Brotli: um bloco de navegação que era 64% dos bytes crus do documento valia ~25% do documento comprimido (~16 KB), enquanto um chunk de JS na mesma página era 254 KB na rede. Quando vale cortar, paga em parse/DOM/hidratação — verificar em TBT/INP, e o que um crawler precisa ler continua server-rendered.

Regra nova — a mais importante, porque este agente edita <head> e __root

Não se muta o que o framework renderizou — muda-se a FONTE do render, em todos os environments que renderizam.

Apagar tag do HTML servido é mismatch: medido, 13 de 4300 nós sobreviveram. Client-only quando o SSR tem de ficar byte-idêntico; client e ssr quando os dois renderizam a tag, porque aí mexer em um só é o mismatch. E o build tem de falhar se o transform não casou onde deveria.

Verify ganhou duas checagens

  • Afirmar o que deve sobreviver, não só o que saiu. Depois de qualquer transform no <head> ou no manifest, procurar o script de entrada do cliente (import("/assets/main-*.js")) no HTML servido. Uma mudança dessa classe produziu documento de 572 KB perfeito, <head> limpo, zero erro de console, zero aviso de build — e página que nunca hidratava. Contar a tag removida teria passado no check.
  • Um run de Lighthouse não é resultado. TBT variou 0,53 s → 5,91 s em 5 runs do mesmo build. Mediana de 5, run de controle no base antes de chamar de regressão, e evidência estrutural (bytes transferidos, requests, conteúdo do chunk, contagem de nós) acima de score.

Onde a profundidade foi parar

skills/knowledge/perf/head-priority-and-hydration.md (novo) — mesmo padrão que o agente já usa com render-location.md e edge-caching.md, com linha no skills/knowledge/INDEX.md. Lá está o que não cabe numa célula de tabela: por que prioridade vence ordem, como se decide entre os dois environments, a quebra silenciosa do manifest passo a passo (getStartManifest poda a rota sem preloads/assets__root__ some → o if (rootRoute) deixa de emitir o script de entrada), e as armadilhas de medição — preview local sem compressão, build local de main ≠ build deployed de main (307 kB de diferença que não existe), cache persistido do worker, warmup de cache segmentado por device.

skills/knowledge/perf/ não está na lista de vendorização do scripts/sync-skills.ts (só tanstack/* e vtex/*), então o arquivo novo não gera drift.

Diff

agents/perf-optimizer.md +28 −3
skills/knowledge/perf/head-priority-and-hydration.md +115 (novo)
skills/knowledge/INDEX.md +1

Repo público: passei um scan por identificador de cliente, URL interna e path de repo privado antes de subir — nada vazou. Se preferirem atribuição explícita à migração de origem, é só pedir que eu adiciono.

🤖 Generated with Claude Code


Summary by cubic

Updates the perf-optimizer agent catalog with measured corrections to two existing instructions and new coverage for the case where LCP is bad while everything else is green.

  • uses-responsive-images now recommends aspectRatio instead of explicit width/height, since in CMS-driven components width is often the CDN resize target and writing the intrinsic width forces full-resolution downloads.
  • Third-party deferral now sequences after LCP/CLS fixes — deferring while LCP is still > 4 s keeps the work inside the TBT window and dropped a measured score from 44 to 30.
  • New catalog rows cover unused-javascript in the own eager bundle (attribute via .js.map mappings, never sourcesContent; don't split per module), unsized-images/CLS on CMS banners (content is usually the gap, not the component), and dom-size (rank by transfer bytes and node count, not raw byte share).
  • Adds the rule to never mutate what the framework rendered — change the render source in every environment that renders it, and make the build fail if a transform didn't match.
  • Verification now asserts what must survive (the client entry script in served HTML) and treats one Lighthouse run as non-resultative (use median of 5 with a base-branch control).
  • New knowledge file skills/knowledge/perf/head-priority-and-hydration.md documents why priority beats document order, how to patch client vs. ssr environments safely, the silent hydration-break failure mode, and measurement traps.

Migration

  • Load the new knowledge file when touching <head> transforms or the eager bundle; it replaces the need to restate the mechanics in commit messages.

Written for commit 2b4f8d2. Summary will update on new commits.

Review in cubic

O catálogo cobre bem opportunity de terceiros (embed, fonte, cache header), mas
não tinha nada para o caso em que o LCP é ruim com todo o resto verde — e duas
das instruções existentes estão medidas como armadilha.

CORREÇÕES em linhas que já existiam:

- `uses-responsive-images` mandava "Add explicit width/height". Em componente
  dirigido por CMS o `width` costuma ser TAMBÉM o alvo de resize passado para o
  CDN de imagem, então escrever a largura intrínseca faz cada imagem baixar em
  resolução cheia. A reserva de caixa correta é `aspectRatio`.
- `prioritize-lcp-image` parava em eager + fetchpriority. É necessário e não
  suficiente: se o Load Delay continuar alto, a imagem está perdendo disputa de
  banda para `modulepreload` (prioridade High) e nenhuma prioridade declarada no
  markup resolve — o browser agenda por prioridade, não por ordem de documento.
- a regra de terceiros mandava deferir para `load` incondicionalmente. Falta o
  sequenciamento: com LCP > 4 s o TBT é medido FCP->TTI, então o trabalho
  deferido continua dentro da janela. Medido: score 44 -> 30. LCP/CLS primeiro.

LINHAS NOVAS:

- `unused-javascript` do NOSSO lote eager: atribuir antes de refatorar, decodificando
  as `mappings` do `.js.map` e nunca `sourcesContent` (mede o arquivo original e
  superestimou um pacote em 5x: 499 966 B contra 101 631 B). Procurar módulo
  dev/admin alcançável por import estático — um comentário dizendo "só carrega
  quando…" não é gate, só `import()` é — e código de primeira parte que a lista
  de exclusão do glob "exclui" mas um import nomeado traz de volta. Não splitar
  por módulo: medido, o JS total quase não mudou (1,727 -> 1,737 MB) e virou 27
  requests a mais sem compressão de stream, score local 42 -> 20.
- `unsized-images` / CLS de banner de CMS: checar o componente primeiro; se ele já
  aceita dimensão, o que falta é conteúdo (1192 de 1198 banners sem valor). Ler a
  dimensão real do header do arquivo com `Range: bytes=0-65535`. Se o conteúdo é
  regerado por bot, o fix tem de ser script de post-generate — um refresh de
  conteúdo desfez em silêncio um fix de CLS já mergeado (0,00 -> 0,95, reproduzido
  duas vezes).
- `dom-size`: ranquear por bytes TRANSFERIDOS e contagem de nós, nunca por share
  de bytes crus. Markup SSR repetitivo é o caso ótimo do Brotli — um bloco de
  navegação que era 64% dos bytes crus do documento valia ~25% do documento
  comprimido (~16 KB), enquanto um chunk de JS na mesma página era 254 KB na
  rede. E quando vale cortar, paga em parse/DOM/hidratação: verificar em TBT/INP.

REGRA NOVA, e é a mais importante porque este agente edita `<head>` e `__root`:
**não se muta o que o framework renderizou — muda-se a FONTE do render, em todos
os environments que renderizam.** Apagar tag do HTML servido é mismatch: 13 de
4300 nós sobreviveram. Client-only quando o SSR tem de ficar byte-idêntico;
client E ssr quando os dois renderizam a tag, porque aí mexer em um só É o
mismatch.

VERIFY ganhou duas checagens:

- afirmar o que deve SOBREVIVER, não só o que saiu: depois de qualquer transform
  no `<head>` ou no manifest, procurar o script de entrada do cliente no HTML
  servido. Uma mudança dessa classe produziu documento de 572 KB perfeito,
  `<head>` limpo, zero erro de console, zero aviso de build — e página que nunca
  hidratava. Contar a tag removida teria passado.
- um run de Lighthouse não é resultado: TBT variou 0,53 s -> 5,91 s em 5 runs do
  mesmo build. Mediana de 5, run de controle no base antes de chamar de
  regressão, e evidência estrutural acima de score.

A mecânica profunda vai para `skills/knowledge/perf/head-priority-and-hydration.md`
(padrão que o próprio agente já usa com `render-location.md` e `edge-caching.md`),
com linha no `skills/knowledge/INDEX.md`. Fica lá o que não cabe numa linha de
tabela: por que prioridade vence ordem, como os dois environments se decidem, a
quebra silenciosa do manifest passo a passo, e as armadilhas de medição (preview
local sem compressão, build local de main != build deployed de main, cache
persistido do worker, warmup de cache segmentado).

Números de before/after medidos numa migração real. Sem identificar o site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants