- Fundamentos: O que é Performance e por que Importa
- Core Web Vitals: LCP, INP e CLS na Prática
- Entrega & Rendering: CDN, Lazy Loading e Estratégias de Render
- JavaScript: Seu Bundle Está Pesado Demais
- CSS: Critical Path e Layout Thrashing
- DevTools: Explorando a Performance no Navegador
- Monitoramento: Medindo o que Importa em Produção
Os Core Web Vitals são três métricas definidas pelo Google para medir a qualidade real da experiência do usuário. Fazem parte do Page Experience signal e influenciam o ranking no Search desde 2021.
Não confunda "boas métricas no Lighthouse" com "bons Core Web Vitals". O Lighthouse roda em condições de laboratório controladas. Os CWV que o Google usa para ranking vêm do Chrome User Experience Report (CrUX) — dados reais de usuários reais, em dispositivos e conexões reais.
As três métricas medem dimensões diferentes da experiência:
Largest Contentful Paint: quando o maior elemento visível terminou de carregar. É a métrica mais próxima de "quando a página parece pronta".
Interaction to Next Paint: quanto tempo entre uma interação do usuário e o próximo frame pintado refletindo essa interação.
Cumulative Layout Shift: quanto o conteúdo "pula" durante o carregamento. Botões que se movem, texto que se desloca.
LCP — Largest Contentful Paint
LCP mede quando o maior elemento de conteúdo visível na viewport terminou de renderizar.
Imagens (img, background-image, image em SVG), elementos video (via poster) e blocos de texto (headings, parágrafos). O browser rastreia o maior e atualiza conforme novos elementos aparecem.
Na primeira interação do usuário (scroll, clique, teclado). O valor final é o último elemento que apareceu antes dessa interação — elementos que chegam depois não afetam o LCP.
- ≤ 2.5s — Bom
75% dos usuários precisam estar nessa faixa para o site ter CWV verde no Search Console.
- 2.5s – 4.0s — Precisa melhorar
Experiência degradada. Usuários mobile em conexões lentas provavelmente estão nessa faixa.
- > 4.0s — Ruim
Impacto negativo em ranking, bounce rate e conversão. Ação urgente necessária.
O que move o LCP
O browser precisa encontrar a imagem LCP o mais cedo possível no HTML. Se for injetada via JS ou CSS, só é descoberta após esses arquivos serem baixados e executados — tarde demais.
<!-- ✅ Descoberta imediata — está no HTML -->
<img src="/hero.jpg" fetchpriority="high" alt="Hero" />
<!-- ❌ Descoberta tardia — só aparece após JS executar -->
<div id="hero"></div>
<script>document.getElementById('hero').innerHTML = '<img src="/hero.jpg">'</script>O browser atribui prioridade baixa a imagens fora do viewport por padrão. Use fetchpriority="high" na imagem LCP para forçar prioridade máxima desde o início.
<img
src="/hero.webp"
fetchpriority="high"
loading="eager"
width="1200"
height="600"
alt="Hero"
/>Nunca use loading="lazy" na imagem LCP — instrui o browser a adiar o download, exatamente o oposto do que você quer.
CSS e JS síncronos no head bloqueiam o browser de renderizar qualquer coisa. Cada milissegundo de bloqueio atrasa o LCP.
- link rel=stylesheet sem preload
CSS externo no head bloqueia toda renderização até ser baixado e processado.
- script síncrono sem defer ou async
Scripts síncronos param o parsing do HTML completamente até serem executados.
- Fontes sem font-display
Sem font-display, o browser pode deixar o texto invisível enquanto a fonte carrega.
Se o servidor demorar para responder (TTFB alto), o LCP sofre diretamente. CDN, caching e SSG/ISR atacam esse problema.
Imagens grandes também atrasam: uma imagem de 2MB numa conexão 4G leva vários segundos. Sirva sempre WebP/AVIF com srcset para entregar o tamanho certo para cada viewport.
<img
src="/hero-800.webp"
srcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w"
sizes="(max-width: 768px) 100vw, 800px"
fetchpriority="high"
alt="Hero"
/>INP — Interaction to Next Paint
INP mede a responsividade da página — quanto tempo entre uma interação do usuário e o próximo frame pintado refletindo essa interação. Em março de 2024 substituiu o FID como Core Web Vital. A diferença fundamental: o FID media apenas o delay do primeiro input. O INP mede todas as interações e reporta o percentil 98.
- ≤ 200ms — Bom
Interação percebida como imediata. O usuário não sente latência.
- 200ms – 500ms — Precisa melhorar
Latência perceptível. Usuários em dispositivos lentos ou com muitas interações simultâneas sentem a demora.
- > 500ms — Ruim
Página percebida como travada. Alta correlação com abandono de formulários e cliques duplos acidentais.
Como o INP é calculado
Cada interação tem três fases que somadas formam o INP:
Tempo entre o evento físico e o início do event handler. Causado por long tasks bloqueando o main thread quando o usuário interage.
Tempo de execução do event handler em si. Handlers pesados com muita lógica ou manipulação de DOM contribuem aqui.
Tempo do browser para calcular layout, estilo e pintar o frame após o handler terminar.
O que causa INP alto
Uma long task é qualquer tarefa que bloqueia o main thread por mais de 50ms. Durante ela, o browser não consegue processar eventos de input — o clique do usuário fica na fila.
// ❌ Long task — bloqueia o main thread
function processLargeList(items) {
return items.map(item => heavyComputation(item));
}
// ✅ Yield para o browser entre chunks
async function processLargeList(items) {
const results = [];
for (const item of items) {
results.push(heavyComputation(item));
await scheduler.yield(); // deixa o browser respirar
}
return results;
}Event handlers que fazem muito trabalho direto: queries no DOM, cálculos complexos, re-renders de listas inteiras. A solução no React é separar o que é urgente do que pode esperar.
// ❌ Recalcula tudo a cada keyup
input.addEventListener('keyup', () => {
const results = filterLargeList(input.value);
renderResults(results); // pode ser 100ms+
});
// ✅ useTransition para updates não-urgentes
const [isPending, startTransition] = useTransition();
setQuery(e.target.value); // urgente — atualiza o input imediatamente
startTransition(() => {
setResults(filterLargeList(e.target.value)); // pode ser interrompido
});Em aplicações React com SSR, a hidratação pode bloquear o main thread por centenas de ms logo após o carregamento, causando INP alto nas primeiras interações do usuário.
Soluções diretas para reduzir o custo de hidratação:
- Streaming SSR + Suspense
Hidrata em partes progressivas, não tudo de uma vez. O usuário pode interagir com seções prontas enquanto outras ainda hidratam.
- useTransition e useDeferredValue
Marca updates de UI como não-urgentes. O React prioriza interações do usuário sobre re-renders pesados.
- React Server Components
Reduz o JS enviado ao cliente — componentes que não precisam de interatividade ficam apenas no servidor.
CLS — Cumulative Layout Shift
CLS mede a estabilidade visual — quanto o conteúdo "pula" enquanto carrega. Um botão que se move quando o usuário está prestes a clicar, texto que se desloca quando uma imagem carrega sem dimensão definida.
- ≤ 0.1 — Bom
Layout estável. Usuário não percebe movimento inesperado de conteúdo.
- 0.1 – 0.25 — Precisa melhorar
Shifts perceptíveis. Botões e links se movem o suficiente para causar cliques errados.
- > 0.25 — Ruim
Layout instável. Alta probabilidade de cliques acidentais e experiência frustrante.
Causas mais comuns
- Imagens sem dimensão definida
O browser não sabe o tamanho antes de baixar. Quando a imagem carrega, empurra todo o conteúdo abaixo. Fix: sempre defina
widtheheight— o browser calcula o aspect-ratio automaticamente e reserva o espaço. - Anúncios e embeds sem espaço reservado
Slots de anúncios que aparecem do nada são os maiores contribuintes de CLS em produção. Reserve o espaço com
min-heightantes do anúncio carregar. - Fontes web causando FOUT
Quando a fonte web substitui o fallback, o tamanho do texto pode mudar e causar shift. Use
size-adjustno font-face do fallback para aproximar as métricas tipográficas. - Conteúdo dinâmico acima de conteúdo existente
Banners de cookies, toasts e notificações que aparecem no topo empurram conteúdo para baixo. Use posição fixa ou reserve o espaço antecipadamente.
- Animações nas propriedades erradas
Animar
top,left,marginouheightcausa reflow e pode gerar CLS. Usetransform: translate()— não afeta o layout, roda só no compositor.
Teste seus valores
Agora que você conhece as três métricas e seus thresholds, simule os valores reais do seu site. Arraste cada slider e veja instantaneamente o diagnóstico — com os mesmos critérios que o Google usa para Page Experience.
Acesse o PageSpeed Insights para o seu domínio. Na seção "Descubra o que seus usuários reais estão experienciando", você encontra o p75 de LCP, INP e CLS dos últimos 28 dias — esses são os valores que o Google usa para ranking.
Verificador de Core Web Vitals
Largest Contentful Paint
Interaction to Next Paint
Cumulative Layout Shift
Arraste os sliders para simular seus valores reais
Como debugar Core Web Vitals
Identificou um problema no checker acima? Use essas ferramentas para encontrar a causa raiz:
Abra o DevTools e acesse a aba Performance. Grave uma sessão e procure por:
Long tasks marcadas em vermelho — impacto direto no INP. Layout shifts marcados na linha "Experience" — impacto direto no CLS. O LCP candidate é identificado automaticamente na linha "Timings".
O painel Performance Insights (More tools) oferece análise automática com sugestões contextuais — melhor ponto de entrada para quem está começando.
Combina dados de lab (Lighthouse) com dados de campo (CrUX) para o seu domínio. Indispensável para ver o impacto real em usuários.
O relatório mostra o p75 de cada métrica para os últimos 28 dias — que é exatamente o que o Google usa para Page Experience. Dados de lab e de campo frequentemente divergem: um LCP bom no Lighthouse pode ser ruim para usuários reais em 3G.
import { onLCP, onINP, onCLS } from 'web-vitals/attribution';
onLCP(({ value, attribution }) => {
// attribution.element: qual elemento é o LCP
// attribution.url: URL do recurso (imagem)
console.log('LCP:', value, attribution.element);
});
onINP(({ value, attribution }) => {
// attribution.eventTarget: o que o usuário clicou
// attribution.processingDuration: tempo do handler
console.log('INP:', value, attribution.eventTarget);
});
onCLS(({ value, attribution }) => {
// attribution.largestShiftTarget: o elemento que causou o shift
console.log('CLS:', value, attribution.largestShiftTarget);
});A versão attribution não só mede — ela diz o que causou. Em vez de saber que o LCP está em 3.5s, você sabe que é a imagem /hero.jpg e que 2s disso é TTFB.
No próximo artigo, exploramos Entrega & Rendering — CDN, estratégias de render (SSR, SSG, ISR, Streaming), lazy loading, resource hints e compressão.
As minhas notas
Selecione um trecho e use Destacar no menu (somente neste aparelho).