De 44 a 83 no Lighthouse: destravando o scroll deste portfólio
Este portfólio tinha um problema clássico: travava durante o scroll. O Lighthouse mobile marcava 44 de performance, com Total Blocking Time de 1.890 ms — quase dois segundos de main thread bloqueada. Esta nota documenta as quatro causas encontradas e o que cada correção rendeu, tudo medido pela mediana de três execuções no Lighthouse CI.
Causa 1: hidratação em bloco no meio do scroll
Quatro seções interativas pesadas (dashboard, modos de leitura, timeline e
estudos de caso) eram montadas de uma vez, num único IntersectionObserver com
rootMargin de 1800px. Resultado: um render síncrono gigante disparava quando o
usuário ainda estava a quase duas telas de distância — exatamente no meio do
gesto de scroll.
A correção: cada seção ganhou seu próprio observer com rootMargin de 400px, e
o mount pesado passou a ser agendado via requestIdleCallback, deixando o frame
de scroll em andamento terminar antes do commit do React.
Causa 2: WebGL desperdiçando a main thread
O fundo do hero (um shader WebGL) chamava getBoundingClientRect() a cada frame
— um forced reflow 60 vezes por segundo — e o loop de requestAnimationFrame
nunca pausava quando o hero saía da viewport. A correção: leitura de tamanho via
ResizeObserver (com cache), pausa do loop via IntersectionObserver e teto de
30 fps — é um fundo sutil, não precisa de mais.
Causa 3: o SDK de monitoramento era metade do bundle
A maior surpresa veio da medição do bundle: o SDK client de monitoramento de erros representava 45% do chunk inicial (415 KB → 228 KB sem ele). O Session Replay sozinho custava ~50–60 KB mais gravação contínua de DOM na main thread.
Removi o Replay primeiro (TBT −510 ms, medido) e depois movi o monitoramento inteiro para o servidor: erros de runtime continuam capturados onde importa, e a performance de campo já era coberta por Web Vitals RUM. Vale registrar o que não funcionou: os flags de tree-shaking do SDK são ignorados pelo Turbopack, e o lazy-import duplicava o bundle em vez de reduzi-lo. A única alavanca real foi remover a dependência do caminho crítico.
Causa 4: a animação do título segurava o LCP
O título do hero usa um efeito de "scramble" que, durante a animação, deixa o
texto real com color: transparent. Em mobile emulado, o maior elemento da
página só estabilizava quando a animação terminava — empurrando o LCP simulado
para ~4,6 s. A correção: pular a animação em viewports pequenos e ponteiros
grossos, a mesma política já usada para o WebGL.
Resultado
| Métrica | Antes | Depois |
|---|---|---|
| Performance (mobile) | 44 | 83 |
| Total Blocking Time | 1.890 ms | ~80 ms |
| LCP observado | — | 1,3 s |
Duas lições ficaram. Primeiro, medir com mediana de múltiplas execuções: um único run de Lighthouse em CI varia até 10 pontos, e decisões tomadas sobre um run único quase me fizeram descartar otimizações que funcionavam. Segundo, o custo de performance raramente está onde parece — ninguém suspeitava que o monitoramento de erros fosse o item mais pesado da página.