Otimização de Core Web Vitals no Frontend: De 50 a 95+ no Lighthouse
Por que Core Web Vitals impacta diretamente o SEO e a receita
Em 2026, o Google utiliza Core Web Vitals como sinal principal de ranking de busca há mais de três anos. INP (Interaction to Next Paint) substituiu FID como a nova métrica, e as três métricas—LCP, CLS, INP—determinam diretamente se seu site ganha tráfego e conversões.
| Desempenho de métricas | Impacto no ranking SEO | Taxa de rejeição | Taxa de conversão |
|---|---|---|---|
| Bom (todas aprovam) | Aumento de ranking 15-30% | Abaixo de 35% | Acima de 3.5% |
| Precisa de melhoria | Sem mudança significativa | 40-60% | 1.5-3% |
| Ruim (todas falham) | Queda de ranking 20-40% | Acima de 70% | Abaixo de 1% |
Um estudo em sites de e-commerce mostra: cada redução de 100ms no LCP melhora a conversão em 0.7%; cada redução de 0.1 no CLS aumenta o tempo de permanência do usuário em 15%. O desempenho não é mais uma métrica técnica—é uma métrica de negócio.
Estratégias de otimização de LCP
LCP (Largest Contentful Paint) mede o tempo de renderização do maior elemento de conteúdo. Em 2026, o limite de aprovação é dentro de 2.5 segundos, e o padrão excelente é dentro de 1.8 segundos.
Otimização de imagens
As imagens são tipicamente o elemento LCP, e otimizá-las produz os resultados mais significativos:
<picture>
<source
srcset="hero-image.avif?w=800 800w, hero-image.avif?w=1200 1200w, hero-image.avif?w=1600 1600w"
type="image/avif"
/>
<source
srcset="hero-image.webp?w=800 800w, hero-image.webp?w=1200 1200w, hero-image.webp?w=1600 1600w"
type="image/webp"
/>
<img
src="hero-image.jpg?w=1200"
srcset="hero-image.jpg?w=800 800w, hero-image.jpg?w=1200 1200w, hero-image.jpg?w=1600 1600w"
sizes="(max-width: 768px) 100vw, 1200px"
alt="Hero banner"
width="1200"
height="600"
fetchpriority="high"
decoding="async"
loading="eager"
/>
</picture>
Pontos-chave: AVIF优于WebP优于JPEG;use fetchpriority="high" para aumentar a prioridade da imagem LCP; sempre declare width e height para prevenir CLS; nunca use carregamento preguiçoso em imagens LCP.
Otimização de fontes
O carregamento de fontes personalizadas é uma causa comum de atrasos no LCP:
<link
rel="preload"
href="/fonts/inter-var-subset.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<style>
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-subset.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC;
}
</style>
font-display: swap garante que o texto esteja sempre visível; unicode-range subsetting reduz o tamanho da fonte em 60-80%; preload inicia as requisições de fonte antecipadamente.
Otimização de CSS
Inclua o CSS crítico inline, carregue o CSS não crítico de forma assíncrona:
<style>
/* Critical CSS - inlined in <head> */
.hero{display:flex;align-items:center;justify-content:center;min-height:60vh;background:linear-gradient(135deg,#667eea,#764ba2)}
.hero h1{font-size:clamp(2rem,5vw,4rem);color:#fff;margin:0}
.hero p{font-size:clamp(1rem,2vw,1.5rem);color:rgba(255,255,255,0.9)}
</style>
<link rel="preload" href="/styles/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="/styles/non-critical.css" /></noscript>
Otimização do servidor
Combinação de CDN + Edge Caching + Streaming:
// Next.js Streaming SSR example
export default async function Page() {
const data = await fetch('https://api.example.com/hero', {
next: { revalidate: 3600 }
}).then(r => r.json());
return (
<main>
<Suspense fallback={<HeroSkeleton />}>
<HeroSection data={data} />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<DeferredContent />
</Suspense>
</main>
);
}
O edge caching retorna requisições LCP em menos de 50ms; Streaming evita que a primeira pintura seja bloqueada por dados lentos.
Estratégias de correção de CLS
CLS (Cumulative Layout Shift) mede a estabilidade visual. Em 2026, o limite de aprovação é dentro de 0.1, e o padrão excelente é dentro de 0.05.
Técnicas de estabilidade de layout
.card {
contain: layout style paint;
content-visibility: auto;
contain-intrinsic-size: 0 320px;
}
.ad-slot {
min-height: 250px;
background: #f5f5f5;
}
.skeleton {
min-height: 200px;
animation: shimmer 1.5s infinite;
}
contain isola o escopo de impacto do layout; content-visibility: auto pula a renderização de conteúdo fora da tela; min-height reserva espaço para conteúdo dinâmico.
Atributos de dimensões de imagem
<!-- Wrong: missing dimensions -->
<img src="product.jpg" alt="Product" />
<!-- Correct: declare dimensions -->
<img
src="product.jpg"
alt="Product"
width="400"
height="300"
style="width: 100%; height: auto;"
/>
<!-- Responsive aspect ratio -->
<img
src="product.jpg"
alt="Product"
style="aspect-ratio: 4/3; width: 100%; height: auto;"
/>
Tratamento de conteúdo dinâmico
function loadComments(postId) {
const container = document.getElementById('comments');
// Reserve minimum height
container.style.minHeight = '300px';
fetch(`/api/comments/${postId}`)
.then(r => r.json())
.then(comments => {
// Batch update to avoid multiple reflows
const fragment = document.createDocumentFragment();
comments.forEach(comment => {
const el = document.createElement('div');
el.className = 'comment-item';
el.textContent = comment.text;
fragment.appendChild(el);
});
container.innerHTML = '';
container.appendChild(fragment);
container.style.minHeight = '';
});
}
Prevenção de CLS por carregamento de fontes
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-subset.woff2') format('woff2');
font-display: optional;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
/* Or use size-adjust to unify fallback and custom font sizes */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107.06%;
ascent-override: 90%;
descent-override: 22%;
}
font-display: optional evita completamente o CLS por troca de fontes; size-adjust alinha o tamanho da fonte de fallback com a fonte personalizada.
Estratégias de otimização de INP
INP (Interaction to Next Paint) mede a responsividade das interações. Em 2026, o limite de aprovação é dentro de 200ms, e o padrão excelente é dentro de 100ms.
Otimização de manipuladores de eventos
// Wrong: synchronous long task blocks main thread
searchInput.addEventListener('input', (e) => {
const results = heavyFilter(e.target.value, allData); // may take 500ms
renderResults(results);
});
// Correct: debounce + yield to main thread
searchInput.addEventListener('input', debounce(async (e) => {
const value = e.target.value;
// Use scheduler.yield to yield to the main thread
await scheduler.yield();
const results = heavyFilter(value, allData);
renderResults(results);
}, 150));
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
Gerenciamento da thread principal
async function processLargeDataset(data) {
const CHUNK_SIZE = 50;
const results = [];
for (let i = 0; i < data.length; i += CHUNK_SIZE) {
const chunk = data.slice(i, i + CHUNK_SIZE);
results.push(...processChunk(chunk));
// Yield to main thread every 50 items
if (i % (CHUNK_SIZE * 10) === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
return results;
}
Web Workers para computação pesada
// main.js
const worker = new Worker('/workers/search-worker.js');
worker.postMessage({ type: 'SEARCH', query: userInput, data: largeDataset });
worker.onmessage = (event) => {
if (event.data.type === 'SEARCH_RESULT') {
renderResults(event.data.results);
}
};
// workers/search-worker.js
self.onmessage = (event) => {
if (event.data.type === 'SEARCH') {
const results = performHeavySearch(event.data.query, event.data.data);
self.postMessage({ type: 'SEARCH_RESULT', results });
}
};
function performHeavySearch(query, data) {
return data.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
}
requestAnimationFrame e Scheduler API
// 2026 recommended: Scheduler API
async function handleScroll() {
const pendingUpdates = collectScrollUpdates();
// Yield to main thread, ensure next frame paints
await scheduler.yield();
// Execute visual updates at the right time
requestAnimationFrame(() => {
applyScrollUpdates(pendingUpdates);
});
}
// Compatibility fallback
function yieldToMain() {
return new Promise(resolve => {
if ('scheduler' in window && 'yield' in scheduler) {
scheduler.yield().then(resolve);
} else {
setTimeout(resolve, 0);
}
});
}
Configuração de medição e monitoramento
Lighthouse CI
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci && npm run build
- name: Lighthouse CI
uses: treosh/lighthouse-ci-action@v12
with:
urls: |
http://localhost:3000/
uploadArtifacts: true
budgetPath: ./lighthouse-budget.json
configPath: ./lighthouserc.json
// lighthouse-budget.json
[
{
"path": "/*",
"options": {
"first-contentful-paint": ["warn", { "maxNumericValue": 1800 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"interactive": ["warn", { "maxNumericValue": 200 }]
}
}
]
Biblioteca web-vitals
import { onLCP, onCLS, onINP } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
url: location.href,
timestamp: Date.now()
});
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/vitals', body);
} else {
fetch('/api/vitals', { body, method: 'POST', keepalive: true });
}
}
onLCP(sendToAnalytics);
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
Monitoramento de Usuários Reais (RUM)
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log({
name: entry.name,
duration: entry.duration,
startTime: entry.startTime,
entryType: entry.entryType
});
reportToAnalytics({
metric: entry.name,
value: entry.duration,
path: location.pathname,
connection: navigator.connection?.effectiveType,
deviceMemory: navigator.deviceMemory
});
}
});
observer.observe({
type: 'largest-contentful-paint',
buffered: true
});
observer.observe({ type: 'layout-shift', buffered: true });
observer.observe({ type: 'event', buffered: true });
5 armadilhas comuns e soluções
Armadilha 1: Uso excessivo de carregamento preguiçoso
<!-- Wrong: LCP image uses lazy loading -->
<img src="hero.jpg" loading="lazy" alt="Hero" />
<!-- Correct: LCP image uses eager + fetchpriority -->
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="Hero" />
Nunca use carregamento preguiçoso em elementos LCP—fazer isso atrasa a renderização em 200-500ms.
Armadilha 2: Ignorar o CLS de fontes
O carregamento de fontes altera as dimensões do texto, causando deslocamentos de layout. Sempre use size-adjust ou font-display: optional.
Armadilha 3: Scripts de terceiros não otimizados
<!-- Use Partytown to move third-party scripts to a Web Worker -->
<script type="text/partytown" src="https://analytics.example.com/script.js"></script>
<script>
partytown = {
forward: ['dataLayer.push', 'gtag']
};
</script>
Armadilha 4: Não tratar deslocamentos de layout por conteúdo dinâmico
Anúncios, listas de recomendação e outros conteúdos assíncronos devem reservar min-height, ou cada carregamento produzirá CLS.
Armadilha 5: Executar computação pesada na thread principal
Operações de busca, ordenação e filtragem que excedam 50ms devem ser movidas para um Web Worker, ou INP inevitavelmente ultrapassará o limite.
10 itens de solução de problemas de erros
| # | Sintoma | Causa possível | Método de solução de problemas |
|---|---|---|---|
| 1 | LCP > 4s | Imagem LCP não otimizada | Verificar elemento LCP no painel Performance do DevTools, conferir formato e tamanho da imagem |
| 2 | Imagem LCP carrega lentamente | Falta de CDN ou estratégia de cache | Verificar cache-control e cf-cache-status nos Response Headers |
| 3 | CLS > 0.25 | Imagens sem width/height | Verificar auditoria CLS do Lighthouse para elementos de deslocamento específicos |
| 4 | CLS por fontes | font-display mal configurado | Verificar valor de font-display em @font-face |
| 5 | INP > 500ms | Tarefa longa no manipulador de eventos | Localizar no painel Long Animation Frames do DevTools |
| 6 | INP com picos ocasionais | Script de terceiros bloqueando | Filtrar por URL no painel Performance para identificar scripts de terceiros |
| 7 | FCP normal mas LCP lento | Ordem de carregamento de recursos críticos | Verificar CSS/JS não utilizado no painel Coverage, cascata no painel Network |
| 8 | INP mobile muito pior que desktop | Problemas de manipulação de eventos de toque | Verificar se touchstart/touchend têm passive: true |
| 9 | Travamento ao rolar | Manipulação pesada de eventos de scroll | Verificar se está usando limitação com requestAnimationFrame |
| 10 | Pontuação do Lighthouse instável após otimização | Ambiente de teste inconsistente | Usar Lighthouse CI para condições de teste consistentes |
Estudo de caso real: Página inicial de e-commerce de 50 a 95+
Dados de Core Web Vitals antes e depois da otimização para uma página inicial de e-commerce:
| Métrica | Antes | Depois | Melhoria |
|---|---|---|---|
| Pontuação Lighthouse | 50 | 96 | +92% |
| LCP | 5.2s | 1.4s | -73% |
| FID | 280ms | 45ms | -84% |
| CLS | 0.35 | 0.03 | -91% |
| INP | 450ms | 85ms | -81% |
| TTFB | 1.8s | 0.2s | -89% |
| Tamanho JS primeira tela | 850KB | 180KB | -79% |
| Tamanho CSS primeira tela | 320KB | 28KB | -91% |
Passos de otimização:
- Migração de imagens para AVIF + CDN: LCP de 5.2s para 3.1s
- CSS crítico inline + CSS não crítico assíncrono: LCP para 2.3s
- Subsetting de fontes + font-display: optional: CLS de 0.35 para 0.12
- Atributos de dimensões de imagem + reserva de min-height: CLS para 0.03
- Lógica de busca migrada para Web Worker: INP de 450ms para 120ms
- scheduler.yield + otimização debounce: INP para 85ms
- Edge Caching + Streaming SSR: TTFB de 1.8s para 0.2s, LCP final 1.4s
Ferramentas recomendadas
As seguintes ferramentas online podem ajudá-lo a trabalhar de forma eficiente durante a otimização:
- Formatador JSON — Formata e valida dados JSON de relatórios Lighthouse para identificar rapidamente problemas de desempenho
- Codificador Base64 — Converte ícones pequenos para Base64 inline para reduzir a contagem de requisições HTTP
- Calculadora de Hash — Gera hashes de conteúdo para recursos estáticos para implementar estratégias precisas de invalidação de cache
Resumo: A otimização de Core Web Vitals não é uma tarefa única, mas um processo contínuo. Da otimização de imagens e fontes LCP, à estabilidade de layout CLS, ao gerenciamento da thread principal INP, cada métrica requer uma estratégia sistemática. Lembre-se: o objetivo da otimização de desempenho não é uma pontuação perfeita, mas uma experiência fluida para os usuários. Quando sua pontuação do Lighthouse vai de 50 para 95+, você ganha não apenas melhores rankings de SEO, mas melhorias reais na taxa de conversão.
Experimente estas ferramentas executadas localmente no navegador — nenhum cadastro necessário →