SEO Técnico

Como melhorar o LCP no Next.js: guia pelas 4 subpartes

Publicado em

Para melhorar o LCP no Next.js, descubra primeiro em qual das quatro subpartes o tempo está sendo gasto: TTFB, resource load delay, resource load duration ou element render delay — cada uma tem fixes diferentes, e otimizar a subparte errada não muda nada. Na prática, os quatro movimentos que resolvem a maioria dos casos são: garantir que o elemento LCP está no HTML inicial (Server Component), usar preload no next/image apenas na imagem LCP (o Next.js 16 depreciou priority), carregar fontes com next/font e confirmar o resultado nas subpartes do PageSpeed Insights.

// app/page.tsx — o essencial num único exemplo
import Image from 'next/image'
 
export default function Page() {
  return (
    <section className="relative aspect-[16/9]">
      {/* Server Component: a <img> existe no HTML inicial,
          visível ao preload scanner do navegador */}
      <Image
        src="/hero.webp"
        alt="Descrição real da imagem"
        fill
        sizes="100vw"
        preload // Next.js 16: substitui a antiga `priority`
      />
    </section>
  )
}

A partir do Next.js 16, o config images.qualities é obrigatório no next.config — sem ele o build reclama. O exemplo completo está na seção de resource load duration.

O resto deste guia explica por que cada um desses movimentos funciona, mecanismo por mecanismo — e, mais importante, como descobrir qual deles o seu caso realmente precisa.

O que o LCP mede (e o que não mede)

LCP (Largest Contentful Paint) é o tempo até o maior elemento de conteúdo visível no viewport terminar de renderizar. O threshold oficial é 2,5 segundos ou menos no percentil 75 de dados de campo — não mudou em 2026, apesar de artigos afirmando o contrário (detalhe no FAQ). A referência é a documentação do web.dev.

Dois detalhes que mudam decisões:

Lab não é campo. O número do Lighthouse rodando na sua máquina é um teste sintético; o que o Google usa é o p75 dos usuários reais (CrUX), numa janela móvel de 28 dias. Um fix pode melhorar o lab hoje e só aparecer no campo semanas depois.

O elemento LCP não é necessariamente uma imagem. Pode ser um headline, um bloco de texto ou um card renderizado a partir de dados. Isso importa porque quase todo conteúdo sobre LCP assume imagem — e se o seu elemento LCP é texto, otimizar imagem é esforço zero de retorno.

Este artigo cobre só o LCP — ele faz parte do guia de SEO técnico para Next.js deste site, que contextualiza as três métricas de Core Web Vitals.

Antes de otimizar: descubra qual é o seu elemento LCP

O primeiro passo não é otimizar nada — é identificar o elemento. No Chrome DevTools, abra a aba Performance, grave um carregamento e clique no marcador LCP: ele destaca o elemento exato na página. O PageSpeed Insights também mostra o elemento no insight "Detalhamento da LCP".

Insight Detalhamento da LCP no PageSpeed Insights identificando o elemento LCP da home do seotecnico.dev.br: um parágrafo de texto, com o seletor destacado por uma caixa laranja

O PSI aponta o elemento LCP — aqui, o parágrafo de abertura da home deste site. Repare: é texto, não imagem.

Duas pegadinhas que eu sempre confiro:

  • Mobile e desktop podem ter elementos LCP diferentes. Um layout que esconde o hero no mobile pode fazer o headline virar o LCP lá — e o Google avalia mobile primeiro.
  • O elemento pode mudar durante o carregamento. Se algo re-renderiza maior depois (hidratação, slider), o LCP é remarcado para esse momento. Esse tipo de armadilha vai render um artigo próprio no cluster, sobre os erros que impedem o LCP de melhorar.

Com o elemento identificado, a pergunta vira: onde o tempo está indo?

As 4 subpartes: onde o tempo do LCP está indo

O LCP de uma página se decompõe em quatro fases sequenciais, e o modelo oficial de otimização trata cada uma separadamente:

As 4 subpartes do LCPservidor respondenavegador descobre o recursobytes chegamCSS libera a pinturaTTFBResource load delayDownloadElement render delay0LCPOs delays (em destaque) costumam dominar o LCP — o download raramente é o gargalo.

As 4 subpartes do LCP. Proporções ilustrativas de um caso típico — meça as suas no PageSpeed Insights.

SubparteO que éFix principalOnde ver
TTFBTempo até o primeiro byte do HTMLCache, streaming, CDNPSI, web-vitals
Resource load delayDo TTFB até o navegador começar a baixar o recurso LCPRecurso no HTML inicial + preloadWaterfall
Resource load durationO download em siFormato, sizes, compressãoWaterfall
Element render delayDo fim do download até a pinturaCSS render-blocking, fontesTrace

O insight que muda a estratégia: na maioria dos diagnósticos que eu faço, o gargalo está nos delays (descoberta e render), não no download. O arquivo da imagem — a única coisa que a maioria dos guias otimiza — é uma das quatro fases, e frequentemente a mais curta. É por isso que comprimir a imagem pela terceira vez não move o número.

As quatro seções a seguir atacam uma subparte cada.

TTFB: o piso do seu LCP

Nada renderiza antes do primeiro byte do HTML chegar — o TTFB é um piso que se soma a todo o resto. Se o seu TTFB é 1,8s, o LCP nunca será bom, não importa o que você faça com imagens.

No Next.js, as alavancas em ordem de impacto:

Renderização estática sempre que possível. Página estática servida de CDN elimina o tempo de servidor da equação. No App Router, isso significa não usar APIs dinâmicas (cookies(), headers()) em páginas que não precisam delas.

Streaming quando há data fetching lento. Se a página depende de dados, não segure o HTML inteiro atrás do fetch — envie o shell imediatamente e faça o bloco lento chegar via Suspense:

// app/produtos/page.tsx
import { Suspense } from 'react'
import { ListaProdutos } from './lista-produtos'
 
export default function Page() {
  return (
    <main>
      {/* Shell estático: chega no primeiro byte.
          O candidato a LCP deve estar AQUI, fora do Suspense. */}
      <h1>Produtos</h1>
      <Suspense fallback={<SkeletonLista />}>
        {/* Bloco que depende de fetch: chega via streaming */}
        <ListaProdutos />
      </Suspense>
    </main>
  )
}

Isso melhora o TTFB porque o servidor responde antes de o fetch terminar — mecanismo garantido. O quanto melhora o LCP depende de onde está o elemento LCP: se ele estiver dentro do Suspense, o streaming não resolve (esse caso tem seção própria mais abaixo).

Infraestrutura. Região do deploy perto do público (para site brasileiro, edge/região em SA), cache de dados do Next e CDN na frente. No Next.js 16, o modelo de cache mudou para Cache Components — se você vinha do PPR experimental, vale reler a doc antes de assumir comportamento.

Vindo do PHP em hospedagem compartilhada, essa era a subparte dominante por definição: o servidor renderizava tudo a cada request e o TTFB engolia o budget inteiro. No Next.js com estático/streaming, o gargalo tipicamente migra para as duas próximas subpartes — o que é bom, porque elas estão sob controle do código, não da infraestrutura.

Resource load delay: o navegador precisa descobrir a imagem cedo

O load delay mede quanto tempo o navegador levou para começar a baixar o recurso LCP depois do TTFB. Delay alto quase sempre significa descoberta tardia: o navegador não sabia que o recurso existia.

O mecanismo por trás: o navegador tem um preload scanner — um parser secundário que varre o HTML bruto e antecipa downloads antes mesmo do parse principal terminar. Ele lê markup, não CSS nem JavaScript (web.dev). Uma <img> no HTML da resposta é descoberta imediatamente; uma imagem que só existe dentro de um CSS ou depois de um script executar, não. Os padrões invisíveis ao scanner (background-image, hero atrás de "use client", cadeias de @import) merecem um artigo inteiro dedicado — será o próximo do cluster.

Garantida a descoberta, o segundo passo é prioridade. No Next.js 16:

// components/hero.tsx
import Image from 'next/image'
 
// ❌ Next.js ≤15 — `priority` está depreciada no v16:
// <Image src="/hero.webp" alt="..." fill sizes="100vw" priority />
 
// ✅ Next.js 16:
export function Hero() {
  return (
    <div className="relative aspect-[16/9]">
      <Image src="/hero.webp" alt="..." fill sizes="100vw" preload />
    </div>
  )
}

A prop preload injeta um <link rel="preload"> no <head> e já aplica fetchPriority="high" — não precisa dos dois. E a regra de ouro: uma imagem com preload por página, a que é de fato o elemento LCP. Preload em várias imagens faz elas competirem por banda entre si e com o CSS crítico, o que pode piorar o LCP — melhora quando o gargalo é load delay do elemento certo, atrapalha quando vira competição.

Se o hero precisa mesmo ser background CSS (decisão de design legítima em alguns layouts), avise o navegador manualmente:

// app/layout.tsx (ou no componente da página)
import { preload } from 'react-dom'
 
export default function Layout({ children }: { children: React.ReactNode }) {
  preload('/hero.webp', { as: 'image', fetchPriority: 'high' })
  return <>{children}</>
}

Isso restaura a descoberta antecipada que o background-image perde — mecanismo garantido; a magnitude depende de quão tarde a imagem era descoberta antes.

Resource load duration: agora sim, o arquivo

O load duration é o download em si — a única subparte que a compressão de imagem afeta. Três alavancas no Next.js:

Formato: deixe o next/image negociar. O componente serve AVIF/WebP automaticamente conforme o navegador aceita. Você não precisa converter manualmente — precisa apenas não desligar a otimização (unoptimized) sem motivo.

sizes correto. Com fill, se você omitir sizes, o navegador assume 100vw — e num grid de 3 colunas isso significa baixar uma imagem 3x maior que o necessário. Para o hero full-width, sizes="100vw" está certo; para qualquer outro layout, descreva a realidade:

// components/card-produto.tsx
<Image
  src={produto.imagem}
  alt={produto.nome}
  fill
  sizes="(max-width: 768px) 100vw, 33vw"
/>

qualities no config (obrigatório no v16):

// next.config.ts
import type { NextConfig } from 'next'
 
const config: NextConfig = {
  images: {
    qualities: [50, 75], // 75 para heros, 50 para thumbnails
  },
}
 
export default config

Reduzir quality de 75 para 50 num thumbnail corta bytes sem diferença visual perceptível naquele tamanho — mas o ganho de LCP só existe se o gargalo era duration. Confira no waterfall antes de gastar tempo aqui.

Element render delay: a imagem chegou, falta pintar

O render delay é a subparte menos compreendida: o recurso já baixou, mas a pintura não acontece. O mecanismo: o navegador não constrói a render tree até o CSSOM estar completo — todo CSS é render-blocking por definição (web.dev). Uma imagem que baixou em 400ms pode esperar 2s parada na memória enquanto um stylesheet lento segura a renderização da página inteira.

No Next.js, o caso mais comum de render delay evitável são fontes:

// app/layout.tsx
import { Inter } from 'next/font/google'
 
const inter = Inter({ subsets: ['latin'], display: 'swap' })
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt-BR" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

O next/font baixa a fonte no build e serve do próprio domínio — elimina o request externo render-blocking do Google Fonts e a cadeia de descoberta que ele cria. Isso melhora o render delay porque remove um recurso bloqueante de outra origem do caminho crítico; a magnitude depende da latência até o terceiro e do cache do usuário.

Quando o elemento LCP é texto, display: 'swap' importa dobrado: sem ele, o texto fica invisível durante o período de block da fonte — e texto invisível não conta como renderizado para o LCP.

Neste próprio site a fonte carrega via next/font desde o primeiro deploy. E como o site ainda não aparece no CrUX — que exige um volume mínimo de tráfego — instrumentei um RUM próprio: o web-vitals reporta o LCP real de cada visita, com as 4 subpartes como parâmetros, direto para o GA4 (o código está na seção de verificação). Quando houver dados de campo numa janela estatisticamente útil, atualizo esta seção com a medição.

Outras causas de render delay — fade-in no hero, anti-flicker de A/B testing, cadeias de @import — são anti-patterns, e ficam para o próximo artigo do cluster, sobre os erros que seguram o LCP.

Quando o elemento LCP não é imagem

Se o seu elemento LCP é um headline, o caminho é a seção anterior: fonte via next/font, display: 'swap', e garantir que o texto está no HTML inicial. Não existe "otimizar imagem" aqui — e é por isso que identificar o elemento vem antes de tudo. (É o caso da home deste próprio site: o elemento LCP é o parágrafo de abertura — a decomposição real está na seção de verificação.)

O caso mais difícil é o LCP data-driven: uma lista de produtos, o card principal de um dashboard, o resultado de uma busca. O elemento LCP só existe depois do fetch — o LCP fica refém do banco de dados.

Árvore de decisão: qual é o seu elemento LCP?Qual é o seu elemento LCP?DevTools → Performance → marcador LCPImagemTexto / headlineComponente data-drivenimagem no HTML inicial(Server Component) + preloadnext/font +display: swapcandidato LCP estáticofora do Suspensesizes correto + formatovia next/imagetexto no HTML inicial(fora de client component)cache do dado / skeletondo tamanho do conteúdo

Os três caminhos de fix, conforme o tipo do elemento LCP.

A decisão se resume a três caminhos: LCP imagem → descoberta e prioridade (load delay) + arquivo (duration); LCP texto → fonte e CSS (render delay); LCP data-driven → arquitetura de streaming, descrita a seguir.

As opções, da melhor para a de emergência:

  1. Tirar o candidato LCP do caminho dos dados. Se a página tem um headline ou banner estático que pode ser o maior elemento acima da dobra, deixe-o fora do Suspense — ele chega no primeiro flush e vira um LCP rápido, enquanto os dados fazem streaming abaixo.
  2. Cachear o dado que define o LCP. Se o card principal muda pouco, cache de dados do Next (revalidação periódica) transforma o fetch lento em leitura de cache.
  3. Cuidado com skeletons maiores que o conteúdo. Se o fallback do Suspense é maior que o conteúdo final, o skeleton pode virar o LCP provisório e o conteúdo real remarca o LCP ao chegar — você "paga" o tempo do fetch de qualquer jeito. Skeleton do mesmo tamanho ou menor.

Esse cenário quase não aparece em guias de LCP porque não tem fix de uma linha — é decisão de arquitetura. Mas é o caso real de qualquer app Next.js que renderiza dados acima da dobra.

Como verificar se funcionou

Otimização sem verificação é palpite. O roteiro que eu uso, na ordem:

  1. PSI → subparts. Rode o PageSpeed Insights e olhe a decomposição do LCP: qual subparte domina? Ela aponta qual seção deste guia reler.
  2. DevTools Performance local. Grave um trace com throttling de rede (Fast 4G) e CPU (4x): o marcador LCP mostra o elemento e o waterfall mostra quando o recurso foi descoberto vs. quando terminou.
  3. View-source, não o DOM. Confirme que a <img> do LCP e o <link rel="preload"> gerado pela prop preload estão no HTML da resposta do servidor — o DOM do DevTools mostra o estado depois de JS/CSS, que não é o que o preload scanner viu.
  4. RUM próprio. O pacote web-vitals (build de attribution) reporta o LCP real dos usuários com a subparte dominante. É o que este site faz — o evento vai para o GA4 via GTM, sem backend nenhum (o consentimento é tratado pelo Consent Mode, como qualquer outra tag):
// lib/rum.ts — o código real deste site (simplificado)
import { onLCP } from 'web-vitals/attribution'
 
onLCP(({ value, rating, attribution }) => {
  window.dataLayer?.push({
    event: 'web_vitals',
    metric_name: 'LCP',
    metric_value: Math.round(value),
    metric_rating: rating,
    lcp_element: attribution.target, // v5: era `element` até a v4
    lcp_ttfb: Math.round(attribution.timeToFirstByte),
    lcp_load_delay: Math.round(attribution.resourceLoadDelay),
    lcp_load_duration: Math.round(attribution.resourceLoadDuration),
    lcp_render_delay: Math.round(attribution.elementRenderDelay),
  })
})

No GTM, um trigger de Custom Event web_vitals alimenta uma tag GA4 com esses parâmetros — e as subpartes viram colunas numa exploração. Duas notas de implementação: na v5 do web-vitals o seletor do elemento mudou de attribution.element para attribution.target (em JS puro, o campo antigo devolve undefined sem reclamar); e o callback só dispara quando a métrica finaliza — a página ficar oculta ou a primeira interação — então não espere o evento no load. Se você tem backend com armazenamento, navigator.sendBeacon para um endpoint próprio também funciona; num deploy serverless sem banco, o GA4 é o sink que sobra.

  1. Campo, com paciência. CrUX/GSC usam janela móvel de 28 dias no p75 — não julgue um fix pelo dia seguinte. E se o site é novo, o CrUX pode simplesmente não reportar ainda (exige volume mínimo de tráfego); o RUM próprio do passo 4 cobre esse vazio.

Entre as ferramentas gratuitas de SEO técnico que estou construindo para este site, a primeira leva inclui justamente um checador de Core Web Vitals via CrUX — feito para o passo 5 desse roteiro.

Insight Detalhamento da LCP no PageSpeed Insights para a home do seotecnico.dev.br: subpartes Time to First Byte com 0 ms e atraso na renderização do elemento com 1.280 ms, destacadas por uma caixa laranja

Decomposição real da home deste site (lab, mobile): LCP de texto — por isso só existem TTFB e render delay, e o render delay domina. É o diagnóstico da seção de render delay acima.

© 2026 Henrique Lopes Souza. Todos os direitos reservados. Citações curtas com atribuição e link para o artigo original são bem-vindas.

Perguntas frequentes

2,5 segundos ou menos no percentil 75 dos dados de campo, conforme a documentação oficial do web.dev. Entre 2,5s e 4s, precisa melhorar; acima de 4s é ruim.

Não. Circulam artigos afirmando que o Google baixou o limite para 2,0s em 2026, mas a documentação do web.dev mantém 2,5s. O que mudou em 2026 foi a metodologia de medição do INP e o suporte a soft navigations no CrUX — não os thresholds.

Use preload. O Next.js 16 depreciou a prop priority; a prop nova injeta o link rel=preload no head já com fetchPriority alto. Aplique em uma única imagem por página — a que é de fato o elemento LCP.

Não. Múltiplos preloads competem por banda entre si e com CSS e JavaScript críticos, e podem piorar o LCP. Um candidato real, um preload.

O Lighthouse é lab (uma execução sintética); o Search Console mostra campo (p75 de usuários reais numa janela de 28 dias). Dispositivos lentos e redes ruins que o lab não simula puxam o p75 para cima. O caminho é medição RUM própria para ver o que os usuários reais experimentam.