Destino Dev

Blog

Melhorar LCP com React Server Components e Suspense: streaming, placeholders e revalidação

Por John Pires · 12 de março de 2026

Problema real que resolve

O que afeta o LCP em aplicações React modernas

Largest Contentful Paint (LCP) é um dos sinais mais críticos para percebido de performance: ele mede quando o maior elemento de conteúdo visível é renderizado. Em aplicações SPA tradicionais, o LCP costuma ser penalizado por JavaScript pesado, bloqueio do main thread e carregamento sequencial de dados. Em apps híbridas (SSR + client hydration) o custo aparece quando o servidor espera por todas as chamadas de dados antes de enviar HTML, ou quando o cliente só consegue renderizar o conteúdo depois da hidratação completa.

React Server Components (RSC) e Suspense permitem mudar essa dinâmica: ao produzir HTML no servidor de forma incremental e combinar boundaries de Suspense com placeholders, é possível reduzir o tempo até o primeiro conteúdo significativo. A ideia central é enviar HTML útil mais cedo — especialmente para o maior bloco visível — e hidratar interativos depois, reduzindo LCP.

Cenários reais onde isso importa

Exemplos práticos: uma página de produto cujo LCP é a imagem do produto, um feed de notícias com card principal pesado, ou um dashboard com um gráfico inicial carregado por uma API lenta. Nesses casos, renderizar o maior elemento o quanto antes, mesmo que o restante do conteúdo ainda esteja carregando, gera impacto direto na UX e nas métricas do Core Web Vitals.

Este artigo foca em padrões operacionais testáveis: streaming do servidor, placeholders eficientes, boundaries de Suspense e revalidação controlada de cache. Código real, aplicável em Next.js (App Router) e facilmente adaptável a servidores React que suportam streaming de RSC.

Abordagem ingênua vs abordagem otimizada

Abordagem ingênua

Uma técnica comum e ingênua é aguardar todas as chamadas de dados em um único await no servidor antes de retornar HTML. Exemplo típico:

Problemas desse padrão:

  • Bloqueio do envio do HTML até que tudo esteja pronto.

  • Maior tempo até o LCP, porque o maior elemento geralmente depende de uma chamada lenta.

  • Piora quando há chamadas paralelas demarcadas inadequadamente ou sem timeout/retry.

Abordagem otimizada

O padrão otimizado divide o render em pedaços: prioriza os dados necessários para o maior elemento visível, usa Suspense com placeholders leves, habilita streaming do servidor e aplica revalidação de cache agressiva para chamadas não críticas. Em vez de esperar tudo, você transmite o HTML crítico imediatamente e deixa restante para o streaming incremental.

Benefícios:

  1. Redução direta do tempo até LCP.

  2. Melhor percepção de velocidade (First Contentful Paint + LCP aparecem antes).

  3. Maior tolerância a APIs lentas ou flaky por causa de placeholders e revalidação controlada.

Implementação com código real

Contexto: arquitetura usada nos exemplos

Os exemplos a seguir usam Next.js App Router com React 18+ (Server Components habilitados). Conceitos aplicam-se a qualquer servidor compatível com RSC streaming. Código prático: um endpoint lento, um Server Component que prioriza render do LCP e um Client Component para interatividade leve.

1) API de exemplo (endpoint lento + cache)

O endpoint abaixo simula uma chamada de dados lenta (por exemplo, CDN ou serviço externo). Ele também configura cabeçalhos de cache e mostra uma estratégia simples de revalidação no servidor.

// app/api/product/[id]/route.ts (Next.js route handler)
import { NextResponse } from 'next/server';

export async function GET(request, { params }) {
  const id = params.id;
  // Simula latência
  await new Promise((res) => setTimeout(res, 700));

  const product = {
    id,
    title: `Produto ${id}`,
    image: `https://picsum.photos/seed/${id}/800/600`,
    description: 'Descrição gerada para demonstração. Conteúdo maior para testar LCP.'
  };

  return NextResponse.json(product, {
    headers: {
      // Cache control no edge/CDN: 10s de revalidação (stale-while-revalidate útil em CDN)
      'Cache-Control': 'public, s-maxage=10, stale-while-revalidate=59'
    }
  });
}

O que isso faz: cria um endpoint que demora ~700ms antes de responder, com cache s-maxage para CDN. Na prática, o streaming do servidor pode enviar HTML enquanto a chamada prossegue; o cache reduz latência em requests subsequentes.

2) Server Component com Suspense e placeholder leve

Este Server Component prioriza o bloco que provavelmente será o LCP — a imagem do produto. Ele renderiza um placeholder (skeleton) imediatamente e usa Suspense para streaming incremental.

// app/product/[id]/page.tsx (Server Component com Suspense)
import React, { Suspense } from 'react';
import ProductImage from './product-image';
import ProductMetadata from './product-metadata';

export default async function ProductPage({ params }) {
  // Nota: não await em fetchs aqui para permitir streaming
  const id = params.id;

  return (
    
      {/* Suspense ao redor do bloco que contém a imagem — priorizamos LCP */}
      }>
        {/* ProductImage é um Server Component que faz fetch do mínimo necessário */}
        
      

      {/* Metadata pode carregar depois, com outra boundary */}
      }>
        
    
      {/* Parte interativa como client component */}
     
  );
}


O que acontece: o servidor começam a enviar HTML para o cliente logo após renderizar o skeleton e o que não depender de dados. Quando ProductImage resolve, seu HTML substitui o fallback via streaming. Isso reduz o tempo até o LCP se o browser considerar a imagem substituível pelo conteúdo final.

3) Server Component que busca apenas o necessário para o LCP

ProductImage é minimal: busca só o URL da imagem e as dimensões necessárias. Evite buscar descrição ou reviews que não impactam o LCP.

// app/product/[id]/product-image.tsx (Server Component)
import Image from 'next/image';

export default async function ProductImage({ id }) {
  const res = await fetch(`https://example.com/api/product/${id}`, { cache: 'force-cache' });
  const data = await res.json();

  // Apenas o que for necessário para renderizar a imagem
  const src = data.image;
  const alt = data.title;

  return (
    
  );
}

O que acontece: buscar apenas os campos necessários reduz a latência de I/O e melhora a chance do componente resolver rapidamente. Usar priority em Next.js ajuda o navegador a priorizar o carregamento da imagem que provavelmente será LCP.

4) Client Component para interatividade leve

Parte da página pode ser interativa (ex: botão de adicionar ao carrinho). Mantenha esse código separado como um Client Component para que a hidratação seja limitada ao necessário.

// app/product/[id]/add-to-cart-client.tsx (Client Component)
'use client';
import React, { useState, useTransition } from 'react';

export default function AddToCartClient({ id }) {
  const [isPending, startTransition] = useTransition();
  const [added, setAdded] = useState(false);

  function handleAdd() {
    startTransition(async () => {
      await fetch(`/api/cart/add`, {
        method: 'POST',
        body: JSON.stringify({ id })
      });
      setAdded(true);
    });
  }

  return (
    
      {added ? 'Adicionado' : isPending ? 'Adicionando...' : 'Adicionar ao carrinho'}
    
  );
}

O que acontece: ao separar a interatividade, reduzimos a superfície de hidratação. A página entrega conteúdo crítico primeiro; interações hidratam depois, sem bloquear o LCP.

Testes e validação

Métricas e ferramentas que usar

Ferramentas úteis: Lighthouse (CLI ou DevTools), WebPageTest, Chrome UX Report e métricas de campo (Google Analytics + web-vitals). Em laboratório, use Lighthouse para medir LCP e ver a ordem em que elementos são renderizados; para testes E2E, Playwright pode extrair métricas de performance do navegador.

Valide tanto em condições reais (3G/4G, dispositivos de baixa potência) quanto em ambiente controlado. Lembre-se: redução de LCP no ambiente local nem sempre reflete a experiência real sem throttling apropriado.

Playwright: exemplo para capturar LCP

Exemplo de script Playwright que navega até a página e coleta LCP via PerformanceObserver exposto no page.evaluate. Isso é útil para regressões automatizadas.

// tests/lcp.spec.ts (Playwright)
import { test, expect } from '@playwright/test';

test('mede lcp', async ({ page }) => {
  await page.goto('http://localhost:3000/product/1');

  const lcp = await page.evaluate(() => {
    return new Promise((resolve) => {
      const po = new PerformanceObserver((list) => {
        const entries = list.getEntries();
        const lcpEntry = entries[entries.length - 1];
        if (lcpEntry) {
          resolve(lcpEntry.startTime);
          po.disconnect();
        }
      });
      po.observe({ type: 'largest-contentful-paint', buffered: true });

      // Timeout fallback
      setTimeout(() => resolve(null), 5000);
    });
  });

  console.log('LCP (ms):', lcp);
  expect(lcp).not.toBeNull();
});

O que isso faz: o teste coleta a entrada de LargestContentfulPaint do PerformanceObserver do navegador. Em CI, combine isso com throttling de rede e CPU para simular usuários reais.

Considerações de produção

Caching, revalidação e stale-while-revalidate

Use CDNs com stale-while-revalidate para servir versões em cache enquanto o servidor reconstrói uma nova versão. No Next.js, isso é configurado via cabeçalhos ou via fetch({ next: { revalidate: X } }). Para dados críticos ao LCP, prefira s-maxage curto e prefetch em edge quando aplicável.

Estratégia prática:

  • Campos críticos (imagem, titulo) com revalidação curta e cache no CDN.

  • Campos não críticos (reviews, recomendações) com cache mais longo e revalidação assíncrona.

Armadilhas comuns e dicas práticas

Erros frequentes que vi em time pequenos:

  • Tentar priorizar tudo: se tudo é crítico, nada é. Identifique o LCP real e concentre esforços nele.

  • Skeletons pesados que aumentam o layout shift. Placeholders devem ocupar espaço fixo parecido com o conteúdo final para evitar CLS.

  • Buscar objetos inteiros (ex: product completo) quando você só precisa da imagem. Projete endpoints com payloads mínimos para render inicial.

Tip: sempre medir o LCP com throttling representativo e em dispositivos reais antes de mudar um padrão de production. Pequenas mudanças no streaming podem ter efeitos diferentes dependendo do browser e de conexões reais.

Além disso, cuidado com o uso indiscriminado de priority nas imagens: marcar muitas imagens como priority faz com que o navegador perca a priorização do maior elemento e pode anular ganhos de LCP.

Erros comuns no código

Lista de problemas encontrados em produção e como resolvê-los:

  1. Await global no root do servidor — solução: dividir com Suspense e boundaries.

  2. Client components pesados na rota raiz — solução: isolar e carregar por lazy hydration.

  3. Placeholders que não correspondem ao layout final — solução: reservar dimensões e usar placeholders box-size equivalentes.

Conclusão prática

Resumo direto: para reduzir LCP com React Server Components e Suspense, priorize o HTML do maior elemento visível, isole chamadas de dados por prioridade, entregue placeholders leves que preservem layout e use streaming para enviar conteúdo incrementalmente. Combine isso com políticas de cache pensadas (s-maxage + stale-while-revalidate) para reduzir latência em requisições repetidas.

Checklist prático para aplicar agora:

  • Identifique o LCP real da página (ferramentas: Lighthouse, Playwright, WebPageTest).

  • Separe dados críticos e não críticos; crie endpoints com payload mínimo para o crítico.

  • Use Suspense com placeholders e Server Components pequenos para o bloco LCP.

  • Hidrate interatividade com Client Components isolados e use useTransition para UX suave.

  • Implemente cache CDN com revalidação curta para dados críticos.

Aplicando esses passos você verá redução no LCP e melhora na percepção de performance sem comprometer interatividade. Implementações detalhadas acima permitem começar rápido e iterar conforme métricas de campo cheguem.

Se precisar, posso fornecer uma versão completa do repositório de exemplo (Next.js) com todos os arquivos montados e scripts de teste Playwright integrados.

Assuntos: React, server-components, suspense, Performance, lcp, Next.js

Leia também