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.

Peguei uma rota com topo estático e bloco dinâmico dependente de cookie, liguei Cache Components no Next.js↳Next.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:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfigAntes 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:
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:
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 acessibilidade↳Acessibilidade11 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.
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.











Comentários
Ninguém comentou ainda. Começa a conversa?