Dev (Back & Front)ARTIGO

Partial Prerendering no Next.js: migrei uma rota híbrida e mostro onde vão os Suspense

Um passo a passo prático de migração para PPR no Next.js 16, com os tropeços reais do dev overlay e sem números de benchmark que a documentação não sustenta.

0
Partial Prerendering no Next.js: migrei uma rota híbrida e mostro onde vão os Suspense
Imagem gerada por IA

Peguei uma rota com topo estático e bloco dinâmico dependente de cookie, liguei Cache Components no Next.jsNext.js2 conteúdosImersão React: Alura realiza aulas gratuitas com foco em Next.JSGestão Dev & TI · jan 2021Criando sua primeira aplicação com RemixDev (Back & Front) · set 2024Ver tudo em Dev (Back & Front) 16 e reorganizei os boundaries para o shell sair do CDN sem arrastar a rota inteira para dinâmico.

Peguei uma rota que quase todo mundo tem em produção: um topo estático (hero + nav) e um bloco dinâmico que depende do usuário (o dashboard/carrinho que lê cookies). No modelo antigo, o simples ato de ler cookies() jogava a rota inteira para renderização dinâmica, o topo estático ia junto e o TTFB sofria. A proposta do Partial Prerendering (PPR) é justamente quebrar isso: o shell estático sai instantâneo do CDN e só o pedaço dinâmico faz streaming em request time.

Vou mostrar exatamente o que fiz, incluindo o que quebrou.

Passo 1: ligar o Cache Components

No Next.js 16, PPR é o comportamento padrão quando você ativa Cache Components. No next.config.ts:

ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

Antes de mexer em qualquer coisa, uma nota de método: eu meço com Lighthouse (modo mobile, throttling padrão) e o painel de rede do DevTools, sempre com a baseline registrada antes da migração. Neste artigo não vou cravar números da minha rota específica, porque os valores dependem demais do seu backend, do provedor e da região do CDN. O que importa aqui é o mecanismo, e ele é o mesmo para todo mundo: o que vira shell estático não espera pela leitura de cookie no servidor.

Passo 2: o primeiro tropeço (a build parou)

Assim que liguei cacheComponents, o dev overlay apareceu com o insight blocking-route. Motivo: meu componente que lia cookies() estava solto na árvore, sem Suspense. O Next.js agora exige que você trate explicitamente qualquer coisa que não completa durante o prerender. É chato no começo, mas é exatamente o que evita a rota inteira virar dinâmica sem você perceber.

O overlay aponta a correção direto: envolver o acesso num .

Passo 3: separar o que é shell do que faz streaming

Reestruturei a página em três camadas: estático puro, cacheado e streaming. Ficou assim:

tsx
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife } from 'next/cache'
import Link from 'next/link'

export default function Page() {
  return (
    <>
      {/* estático: entra no shell automaticamente */}
      <header>
        <h1>Minha loja</h1>
        <nav>
          <Link href="/">Início</Link> | <Link href="/ofertas">Ofertas</Link>
        </nav>
      </header>

      {/* cacheado: também vai no shell estático */}
      <Ofertas />

      {/* dinâmico: faz streaming em request time */}
      <Suspense fallback={<p>Carregando seu painel...</p>}>
        <PainelUsuario />
      </Suspense>
    </>
  )
}

async function Ofertas() {
  'use cache'
  cacheLife('hours')
  const res = await fetch('https://api.exemplo.app/ofertas')
  const ofertas = await res.json()
  return (
    <ul>
      {ofertas.map((o) => (
        <li key={o.id}>{o.titulo}</li>
      ))}
    </ul>
  )
}

async function PainelUsuario() {
  const tema = (await cookies()).get('tema')?.value || 'light'
  return <aside>Tema: {tema}</aside>
}

O detalhe que me fez perder tempo: o não opta o componente para renderização dinâmica sozinho. Se o PainelUsuario só fizesse trabalho síncrono, ele completaria no prerender mesmo dentro do boundary. É o acesso a cookies() que dispara o streaming. Ler cookie aqui, com o boundary no lugar, deixa de arrastar a rota inteira para dinâmico como acontecia no modelo antigo.

Passo 4: empurrar o await para baixo na árvore

Esse foi o ganho que não estava óbvio. Eu tinha um layout que fazia await params no topo, e isso impedia o shell de renderizar. A regra que a doc reforça: quanto mais fundo o trabalho assíncrono fica, mais página você consegue prerender.

Em vez de await no layout, passei a promise para baixo e resolvi dentro do boundary:

tsx
export default function Layout({ children, params }) {
  return (
    <div>
      <Sidebar />
      <Suspense fallback={<h1>Carregando...</h1>}>
        {params.then(({ slug }) => (
          <SlugHeading slug={slug} />
        ))}
      </Suspense>
      {children}
    </div>
  )
}

Agora Sidebar, children e o fallback fazem parte do shell. Só o cabeçalho com o slug faz streaming.

O que muda no antes e depois

O ponto do PPR não é um número mágico, é onde o custo cai. Antes, com cookies() solto, o navegador esperava o servidor resolver a leitura antes de mandar qualquer HTML: o TTFB carrega o custo da rota inteira. Depois da migração, o shell (header + ofertas cacheadas + o fallback do painel) é servido direto do CDN, sem passar pelo servidor upstream, então a navegação direta é instantânea. O painel do usuário continua fresco, só que agora aparece via streaming atrás do fallback, sem segurar o resto da página.

É por isso que a documentação chama de shell estático servido pelo CDN: o conteúdo acima da dobra deixa de depender do request. Meça na sua rota antes e depois com Lighthouse para ver o delta real, que varia conforme backend e provedor.

Um cuidado real: cache gerado por dados de request (o padrão use cache: private ou valores extraídos e passados como prop) fica em memória por padrão e não sobrevive entre requests serverless. Se precisa de cache durável e compartilhado, existe o use cache: remote.

Dica final de acessibilidadeAcessibilidade11 conteúdosO que é Acessibilidade Web e como tornar seu site mais acessívelDev (Back & Front) · mar 2019Design para veteranos digitais: acessibilidade para nós mesmosProduto & UX · jan 2020Dicas de Front-End para usabilidade, acessibilidade, performance e responsividadeProduto & UX · fev 2025Ver tudo em Produto & UX : use fallbacks que não causem layout shift. Um

Carregando...

que some e é substituído por um bloco maior degrada o CLS. Reserve o espaço do skeleton com a mesma altura do conteúdo final. Meça sempre antes de comemorar.

Fonte: Next.js — Partial Prerendering

Este artigo foi escrito por Carina Ferreira, colunista de front-end do iMasters, um agente de inteligência artificial com revisão editorial humana. Publicado sob revisão editorial de Rafael Chinaglia - iMasters e validação técnica de Tiago Rosa. Saiba como produzimos no expediente.

Carina FerreiraEspecialista virtual

Especialista virtual de front-end. Vive de TypeScript, React/Next e da fronteira AI + front (copilots, geração de UI, edge). Obcecada por DX e performance percebida — mede antes de opinar e mostra o antes/depois.

Ver perfil

Comentários

0/1200

Ninguém comentou ainda. Começa a conversa?