Files
amare/docs/evidence/lighthouse/README.md

206 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Lighthouse — MAN-109
Medições versionadas porque a rodada anterior (PR #34) sobreviveu apenas como
uma tabela digitada à mão num comentário do Linear: `storage/app/lighthouse` é
gitignored, então não havia contra o quê comparar. Os relatórios brutos pesam
~48 MB por passada e continuam fora do repositório; o que fica versionado são os
resumos, que já carregam a decomposição do LCP e a lista de requests até o LCP.
## Como reproduzir
```bash
# Sobe a imagem de produção. O seed roda no host, não no container:
# ContentSeeder é no-op fora de local/staging/testing (database/seeders/ContentSeeder.php:44),
# então sob APP_ENV=production ele não semeia nada e o Lighthouse mede páginas vazias.
docker build -t amare-app:man109 .
php artisan migrate --force
php artisan db:seed --class=ContentSeeder --force
php artisan media:generate-variants # sem isso o LCP da home infla ~2,5 s
docker run -d --name amare-web -p 8000:8000 -e APP_ENV=production ... amare-app:man109
TARGET=local bash scripts/perf/lighthouse.sh storage/app/lighthouse
```
Cada página é auditada 3 vezes por preset e o relatório de LCP mediano é o
reportado — nunca a média. O LCP se move alguns décimos entre execuções na mesma
build, e uma única execução não sustenta comparação.
## Resultado
Preset mobile padrão do Lighthouse 12.8.2: throttling simulado de 150 ms de RTT,
~1,6 Mbps, CPU 4× mais lenta. Chrome estável do sistema (não o Chromium do
Playwright), imagem de produção local, seeder `ContentSeeder`.
Antes: commit `2e43fde`. As colunas intermediárias mostram cada correção
isoladamente, medida com uma passada completa antes da seguinte entrar.
| página | preset | antes | + fontes e marca | + webp e 720w | + sizes correto | perf antes → depois | bytes antes → depois |
|---|---|---|---|---|---|---|---|
| contato | desktop | 0.53 s | 0.36 s | 0.37 s | **0.36 s** | 100 → 100 | 262 → 106 KiB |
| contato | mobile | 2.55 s | 1.51 s | 1.51 s | **1.50 s** | 97 → 100 | 262 → 106 KiB |
| home | desktop | 0.87 s | 0.67 s | 0.53 s | **0.53 s** | 99 → 100 | 798 → 347 KiB |
| home | mobile | 4.58 s | 3.39 s | 2.87 s | **2.49 s** | 83 → 98 | 798 → 347 KiB |
| portfolio | desktop | 0.83 s | 0.65 s | 0.37 s | **0.36 s** | 99 → 100 | 609 → 259 KiB |
| portfolio | mobile | 3.98 s | 1.58 s | 2.64 s | **1.58 s** | 87 → 100 | 609 → 259 KiB |
| portfolio-detalhe | desktop | 0.97 s | 0.79 s | 0.66 s | **0.65 s** | 99 → 100 | 930 → 508 KiB |
| portfolio-detalhe | mobile | 3.98 s | 3.01 s | 2.71 s | **2.18 s** | 87 → 99 | 785 → 365 KiB |
| servicos | desktop | 0.77 s | 0.36 s | 0.37 s | **0.36 s** | 100 → 100 | 552 → 222 KiB |
| servicos | mobile | 3.68 s | 2.63 s | 1.58 s | **2.03 s** | 89 → 99 | 552 → 222 KiB |
| sobre | desktop | 0.61 s | 0.37 s | 0.37 s | **0.36 s** | 100 → 100 | 322 → 127 KiB |
| sobre | mobile | 3.01 s | 1.58 s | 1.60 s | **1.58 s** | 94 → 100 | 322 → 127 KiB |
FCP cai de 1,51 s para 0,91 s em todas as páginas no mobile. Acessibilidade,
boas práticas e SEO marcam 100 em todas as páginas nos dois presets, antes e
depois. CLS é 0,000 e TBT é 0 ms em todas — as metas de §6.6 para essas três
métricas já passavam e continuam passando.
**Contra a meta de LCP ≤ 2,5 s da §6.6: todas as páginas passam nos dois
presets.** Desktop com folga (máximo 0,65 s). No mobile o pior caso é a home a
2,49 s, ou seja **em cima da linha** — a pior das três execuções dela deu 2,57 s.
Tratar a home como aprovada por margem, não com folga.
Duas colunas intermediárias merecem leitura cuidadosa em vez de conclusão:
- `portfolio` mobile aparece pior na coluna do webp (2,64 s) do que na anterior
(1,58 s). É variância, não regressão: as três execuções daquela passada foram
1,59 / 2,64 / 2,78 s. A página é a mais instável do conjunto e a mediana pulou
de ponta. Na passada final as três deram 1,58 s.
- `servicos` mobile sobe de 1,58 s para 2,03 s da terceira para a quarta coluna,
pelo mesmo motivo (1,58 / 1,58 / 2,03).
É exatamente por isso que o script roda três vezes e reporta a mediana; ainda
assim, diferenças abaixo de meio segundo entre passadas não devem ser lidas como
efeito de uma correção.
Ressalva: a medição é local, então latência de origem e TLS não entram, e o
throttling de rede é simulado. Trate o LCP como piso, não como valor de campo.
## O que cada correção comprou
**Fontes servidas em dobro — 88 KiB fora do caminho crítico.** Bunny entrega
cada peso de EB Garamond em woff2 e woff, e o plugin de fontes emitia uma regra
`@font-face` para cada, woff2 primeiro e woff depois. Duas regras com a mesma
família, peso, estilo e unicode-range fazem a **última** vencer: o navegador
renderizava a partir dos woff e descartava os woff2 pré-carregados.
A prova está no log de rede da home antes da correção: 3 woff em prioridade
`VeryHigh` (88 KiB) — a prioridade mais alta da página, à frente do elemento de
LCP — somados a 3 woff2 em `High` (74 KiB) que só foram baixados porque estavam
em `<link rel="preload">`. 162 KiB de tráfego de fonte para 74 KiB de fonte útil.
woff2 é suportado por todo navegador que este site atende desde 2016, então as
regras woff não eram fallback e sim peso morto. O plugin `amare:fonts-woff2-only`
em `vite.config.js` remove as regras do CSS e do manifest e tira os arquivos do
bundle. É o que derruba o FCP de 1,51 s para 0,91 s em todas as páginas.
**Ativos de marca reencodados — 102 KiB fora do caminho crítico.** O logotipo
era servido a 512 px de largura para renderizar em 48 px (lockup, cabeçalho e
rodapé) e 32 px (mark, home): 84 KiB + 42 KiB com `loading="eager"` em todas as
páginas. Reencodados a 3× do maior render — lockup 149×144 (14 KiB) e mark
191×96 (10 KiB) —, mantendo os mesmos nomes de arquivo para não invalidar cache.
As variantes `on-dark` foram reencodadas junto por consistência; nenhuma view as
usa hoje.
Isto absorve MAN-122: os ativos de marca foram reencodados sem alterar a forma
ou a cor percebida, reduzindo apenas o peso transferido.
Os arquivos versionados aqui são: `2026-08-10-local-antes.md` (commit `2e43fde`),
`2026-08-10-local-etapa-fontes-e-marca.md` (passada intermediária) e
`2026-08-10-local-depois.md` (estado final).
**Variantes WebP nas imagens de conteúdo.** Com fontes e marca resolvidas, o
elemento de LCP de toda página no mobile era a imagem do hero, e o Load Delay de
1,88 s era contenção de banda pura: 143 KiB de JPEG q82 a 960 px, com as três
capas do portfólio somando outros 348 KiB. `ResponsiveImage::generate()` passa a
escrever uma variante `.webp` ao lado de cada variante no formato original, e
`x-media.image` a oferece num `<source type="image/webp">`. O `<img>` continua
apontando para o formato original, então nada quebra em quem não decodifica webp,
e mídia antiga sem irmãos webp renderiza `<img>` puro como antes.
**`sizes` que descreve a realidade.** Nenhuma imagem do site ocupa a viewport
inteira: todas ficam dentro de `container-amare`, que reserva 1,5rem de padding
de cada lado. Declarar `100vw` fazia uma viewport de 412 px em DPR 1,75 pedir
721 px e pular para a variante de 960 para desenhar uma caixa de 637 px — errar
por um pixel custava um terço a mais de bytes. Com `calc(100vw - 3rem)` a home
passa a usar a variante de 720 (**74 KiB**, contra 143 KiB no início).
Foi também por isso que 720 entrou em `ResponsiveImage::WIDTHS`: sem ela o salto
de 480 para 960 é grande demais para a viewport mobile mais comum.
Decomposição final do LCP da home no mobile:
| fase | tempo |
|---|---|
| TTFB | 0,45 s |
| Load Delay | 1,24 s |
| Load Time | 0,10 s |
| Render Delay | 0,69 s |
## O ganho de imagem só aparece depois do deploy regenerar as variantes
Toda a medição acima usou `FILESYSTEM_DISK=local`. Em staging e produção o disco
é `r2`, e lá as variantes `.webp` e a largura 720 **ainda não existem** para a
mídia já publicada. Até `media:generate-variants` rodar, `availableWebpVariants()`
volta vazio, o `<source>` é omitido e o `srcset` do formato original perde a
entrada de 720w — ou seja, nenhum dos dois ganhos de imagem aparece.
O serviço `migrate` do `docker-compose.deploy.yml` roda
`php artisan media:generate-variants` sem condição a cada deploy, depois de
`migrate` e do seed, e o comando só pula um caminho quando o arquivo original não
existe (`MediaGenerateVariantsCommand`) — regenera mesmo quando já há variantes.
Então o primeiro deploy desta branch produz as variantes novas por conta própria.
Enquanto isso não acontece, há um custo sem contrapartida: `x-media.image` faz
agora **8 chamadas `exists()` por imagem** (4 larguras × 2 formatos) contra o
object store, no lugar de 3. Na home são ~32 round trips remotos por request em
vez de ~12. Isso agrava a hipótese não validada abaixo em vez de melhorá-la, e é
mais um motivo para cachear esses metadados.
## Se a home precisar de mais folga
O Load Delay de 1,24 s ainda é contenção: as três capas do portfólio somam
155 KiB e, embora sejam `loading="lazy"` e prioridade `Low`, o navegador as busca
porque entram no limiar de lazy loading da viewport emulada. Os levers restantes,
em ordem de custo:
1. Baixar a qualidade webp de 80 para 75 (medido: 109 → 92 KiB na imagem do
hero a 960 px). Barato, mas mexe na qualidade de imagem de uma marca cujo
posicionamento é acabamento editorial — decisão de produto, não de engenharia.
2. Reduzir o Render Delay de 0,69 s, que agora é a segunda maior fatia e é
trabalho de main thread, não de rede.
3. FrankenPHP worker mode para o TTFB de 0,45 s. Tem gatilho objetivo em
SPEC §22 e não deve ser puxado antes dele.
## Cobertura que este trabalho não tem
Os testes browser não exercitam `srcset` nem `<picture>` com variantes geradas:
nem `ContentSeeder` nem `VisualContentSeeder` geram variantes. Nesses cenários,
`availableVariants()` volta vazio e o componente renderiza `<img>` puro. A
cobertura do caminho com variantes fica nos testes de feature
(`MediaImageComponentTest`).
## Staging
Não medido. A origem de staging responde `303` para
`blocked.teams.cloudflare.com` a partir da rede corporativa da Creditas
("O conteúdo deste site viola a Política de Segurança da Informação"), inclusive
em `/up`. O preflight de HTTP 200 do script barra a execução antes de gastar
minutos auditando páginas de bloqueio.
Duas hipóteses seguem **não validadas** porque só existem com
`FILESYSTEM_DISK=r2`, que é configuração de staging e produção:
- `x-media.image` chama `ResponsiveImage::availableVariants()`,
`availableWebpVariants()` (8 `exists()` somados) e `ResponsiveImage::dimensions()`
(que baixa o arquivo inteiro) a cada render, sem cache. No disco `local` são
leituras de filesystem; no `r2` são ~9 round trips remotos por imagem no lado
servidor, ~36 na home. Teste discriminante: comparar o TTFB de `/contato`
(zero imagens) com o de `/portfolio` (N imagens). Se o TTFB escalar com a
contagem de imagens, está confirmado. **Este é o item mais urgente da lista**,
porque as variantes webp multiplicaram o número de chamadas.
- A mídia vem de `R2_URL`, uma origem cross-origin, e não há `preconnect` no
`<head>` — DNS e TLS entram antes do LCP.
Para medir: rodar `TARGET=staging BASE_URL=<origem> bash scripts/perf/lighthouse.sh`
de uma rede sem o filtro corporativo.