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:

  1. Migração de imagens para AVIF + CDN: LCP de 5.2s para 3.1s
  2. CSS crítico inline + CSS não crítico assíncrono: LCP para 2.3s
  3. Subsetting de fontes + font-display: optional: CLS de 0.35 para 0.12
  4. Atributos de dimensões de imagem + reserva de min-height: CLS para 0.03
  5. Lógica de busca migrada para Web Worker: INP de 450ms para 120ms
  6. scheduler.yield + otimização debounce: INP para 85ms
  7. 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 →

#Core Web Vitals#LCP优化#CLS修复#INP优化#性能指标#2026