Voltar para Artigos
Front-end11 min de leitura

Core Web Vitals e Lighthouse: Do 60 ao 100 em Performance

LCP, CLS e INP com thresholds e causas reais. next/image para evitar CLS, next/font para eliminar FOUT, code splitting e lazy loading, Resource Hints (preconnect, preload) e medição com Lighthouse CI no GitHub Actions.

12 de agosto de 2026

Desde 2021, o Google usa os Core Web Vitals como fator de ranqueamento no Search. Um site lento não apenas frustra usuários — ele perde posição nos resultados orgânicos para concorrentes mais rápidos. Uma queda de 100ms no Largest Contentful Paint pode reduzir conversões em até 8% em e-commerces, de acordo com estudos do Google.

O Lighthouse audita performance, acessibilidade, SEO e boas práticas em uma única ferramenta. Este artigo foca nas métricas de performance: como medi-las, interpretá-las e corrigi-las de forma sistemática.

As Três Métricas Core Web Vitals (2024)

  • LCP (Largest Contentful Paint) — tempo até o maior elemento visível renderizar completamente. Geralmente uma imagem hero, um bloco de texto H1, ou um vídeo. Bom: < 2.5s | Necessita melhoria: 2.5s–4s | Ruim: > 4s. Causas comuns: imagem hero não priorizada, servidor lento, renderização bloqueada por CSS/JS.
  • CLS (Cumulative Layout Shift) — soma dos shifts de layout inesperados durante a vida da página. Um anúncio que aparece empurrando o conteúdo para baixo gera CLS alto. Bom: < 0.1 | Necessita melhoria: 0.1–0.25 | Ruim: > 0.25. Causas: imagens sem dimensões, fontes causando FOUT, iframes sem tamanho definido.
  • INP (Interaction to Next Paint) — substituiu o FID em 2024. Mede a latência de toda interação do usuário (clique, toque, teclado) durante a sessão, não apenas a primeira. Bom: < 200ms | Necessita melhoria: 200ms–500ms | Ruim: > 500ms. Causas: JavaScript pesado bloqueando a main thread, handlers síncronos lentos, reflows desnecessários.

Corrigindo CLS: Dimensões em Imagens e Fontes

O CLS causado por imagens é o mais fácil de corrigir e um dos mais comuns: sem width e height declarados, o browser não reserva espaço para a imagem antes de carregá-la. Quando a imagem chega, ela empurra todo o conteúdo abaixo — um shift visível e irritante. O next/image resolve automaticamente ao exigir width e height (ou fill), além de converter para WebP/AVIF, fazer lazy loading por padrão e exibir um blur placeholder durante o carregamento.

Imagens com Next.js (zero CLS)
import Image from 'next/image';

// ERRADO: sem width/height, o browser não reserva espaço
// Quando a imagem carrega, empurra o conteúdo abaixo → CLS alto
<img src="/hero.jpg" alt="Banner" />

// CORRETO com next/image:
// - Reserva o espaço automaticamente pelo aspect ratio
// - Converte para WebP/AVIF automaticamente
// - Lazy loading por padrão (exceto above-the-fold)
// - Blur placeholder durante o carregamento
<Image
  src="/hero.jpg"
  alt="Banner principal da loja"
  width={1200}
  height={630}
  priority         // Prioriza o carregamento (LCP image — acima da dobra)
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,..."
  sizes="(max-width: 768px) 100vw, 1200px"
/>

// Para imagens de tamanho variável (ex: fill container):
<div className="relative h-64 w-full">
  <Image
    src="/product.jpg"
    alt="Produto"
    fill
    sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
    className="object-cover"
  />
</div>

O CLS causado por fontes é mais sutil: o browser primeiro renderiza o texto com uma fonte de sistema (fallback), e quando a fonte customizada carrega, substitui — causando um reflow que muda o tamanho e posição do texto. Isso é o FOUT (Flash of Unstyled Text). O next/font elimina isso completamente: a fonte é baixada no build e servida localmente, eliminando a requisição externa e tornando o carregamento síncrono com o CSS da página.

font (zero CLS de fonte)
// ERRADO: Google Fonts via <link> — bloqueia renderização e causa FOUT
// <link href="https://fonts.googleapis.com/css2?family=Inter..." />

// CORRETO: next/font baixa a fonte no BUILD e serve localmente
// Zero requisição externa do browser, zero FOUT (Flash of Unstyled Text)
import { Inter, Fira_Code } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',        // Garante texto visível durante o carregamento
  variable: '--font-inter', // Variável CSS para uso no Tailwind/CSS
});

const firaCode = Fira_Code({
  subsets: ['latin'],
  weight: ['400', '500'],
  variable: '--font-fira-code',
});

// No layout.tsx:
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt-BR" className={`${inter.variable} ${firaCode.variable}`}>
      <body className="font-sans">{children}</body>
    </html>
  );
}

Corrigindo LCP: Resource Hints

Os Resource Hints instruem o browser a preparar conexões ou baixar recursos antes de precisar deles. O preconnect elimina a latência de estabelecer a conexão TCP/TLS com servidores de terceiros (CDNs, APIs de fontes) — uma economia de 100-500ms dependendo da latência de rede. O preload sinaliza ao browser para baixar um recurso específico com prioridade máxima, o que é essencial para a imagem LCP: sem preload, o browser só descobre a imagem quando processa o HTML e chega até o <img>, perdendo tempo precioso.

head.tsx ou metadata)
// Para recursos críticos de terceiros (ex: CDN de fontes, API externa)
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt-BR">
      <head>
        {/* preconnect: abre a conexão TCP/TLS com o servidor antes de precisar
            Use para origens de terceiros que você sabe que vai usar */}
        <link rel="preconnect" href="https://cdn.example.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />

        {/* preload: instrui o browser a baixar um recurso com alta prioridade
            Use para a imagem LCP ou font crítica */}
        <link
          rel="preload"
          href="/hero.jpg"
          as="image"
          type="image/jpeg"
        />

        {/* dns-prefetch: resolve o DNS antecipadamente (menos potente que preconnect) */}
        <link rel="dns-prefetch" href="https://analytics.example.com" />
      </head>
      <body>{children}</body>
    </html>
  );
}

Corrigindo INP: Code Splitting e Lazy Loading

O INP alto é causado por JavaScript bloqueando a main thread. O remédio principal é reduzir o bundle inicial: qualquer componente que não é visível acima da dobra (above-the-fold) na primeira renderização não precisa estar no bundle inicial. O dynamic() do Next.js implementa isso com code splitting automático — o componente pesado é baixado apenas quando necessário, mantendo o bundle principal leve e o INP baixo.

Lazy loading de componentes pesados
import dynamic from 'next/dynamic';

// Componentes abaixo da dobra ou condicional: lazy load
// O bundle principal não inclui o código deste componente
const HeavyChart = dynamic(
  () => import('./HeavyChart'),
  {
    loading: () => <div className="animate-pulse h-64 bg-gray-200 rounded" />,
    ssr: false, // Para componentes que dependem de APIs de browser
  }
);

// Modal: só carrega quando o usuário abre
const PaymentModal = dynamic(() => import('./PaymentModal'));

export default function Dashboard() {
  const [showModal, setShowModal] = useState(false);

  return (
    <div>
      {/* HeavyChart: carrega lazy quando entra na viewport */}
      <HeavyChart data={chartData} />

      {/* PaymentModal: carrega apenas quando aberto */}
      {showModal && <PaymentModal onClose={() => setShowModal(false)} />}
    </div>
  );
}

Lighthouse CI no GitHub Actions

O maior erro com Lighthouse é usá-lo como verificação manual e pontual. Uma regressão de performance pode ser introduzida em qualquer PR — um novo componente pesado, uma imagem sem priority, um third-party script adicionado. O Lighthouse CI automatiza a verificação: roda o Lighthouse em cada PR, compara com os thresholds definidos e reprova o CI se alguma métrica piorar. Isso torna a performance uma propriedade do código que o time inteiro mantém, não responsabilidade de uma pessoa.

lighthouse.yml
name: Lighthouse CI

on:
  pull_request:
    branches: [main]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Run Lighthouse CI
        uses: treosh/lighthouse-ci-action@v11
        with:
          urls: |
            http://localhost:3000
            http://localhost:3000/products
          # Falha o CI se as métricas ficarem abaixo dos thresholds
          budgetPath: ./lighthouserc.json
          uploadArtifacts: true

      - name: Comment PR with results
        uses: actions/github-script@v7
        # ... comentário automático no PR com os scores
lighthouserc.json
{
  "ci": {
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "categories:accessibility": ["error", { "minScore": 0.9 }],
        "categories:seo": ["error", { "minScore": 0.9 }],
        "first-contentful-paint": ["warn", { "maxNumericValue": 2000 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "total-blocking-time": ["warn", { "maxNumericValue": 300 }]
      }
    }
  }
}

Sempre execute o Lighthouse em aba anônima e em modo Mobile (throttling de CPU 4x e rede 3G lenta). Esses são os thresholds que o Google usa para ranqueamento. Um score de 100 no Desktop e 50 no Mobile não protege seu SEO.

Conclusão

Performance não é um feature que você adiciona no final — é uma consequência de decisões de arquitetura tomadas durante o desenvolvimento. next/image e next/font eliminam as causas mais comuns de CLS. priority na imagem LCP melhora diretamente o LCP. dynamic() mantém o bundle inicial enxuto e o INP baixo. E o Lighthouse CI no GitHub Actions garante que nenhum PR piore as métricas sem que o time perceba.