Voltar para as notas

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.