206 lines
11 KiB
Markdown
206 lines
11 KiB
Markdown
# 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.
|