Programação & Dev

Next.js 16 PPR: Elimine o tradeoff entre estático e dinâmico e alcance Edge TTFB com dados atualizados

O Partial Prerendering no Next.js 16 resolve o clássico tradeoff de renderização, entregando velocidade estática com dados dinâmicos em uma resposta.

Next.js 16 PPR: Elimine o tradeoff entre estático e dinâmico e alcance Edge TTFB com dados atualizados

O dilema mais antigo no desenvolvimento web — finalmente resolvido

A escolha agonizante entre páginas estáticas ultrarrápidas e conteúdo dinâmico em tempo real definiu por muito tempo a experiência de desenvolvimento no Next.js. Mas e se você nunca mais precisasse fazer concessões? Com o PPR, o dilema entre estático versus dinâmico está finalmente obsoleto.

Considere uma página de produto em um site de e-commerce. O título, a descrição e as imagens quase nunca mudam — eles devem ser estáticos, armazenados em cache na borda da CDN e servidos em milissegundos. A contagem do carrinho do usuário, suas recomendações personalizadas e o estoque em tempo real — esses mudam por requisição e por usuário. Eles precisam ser dinâmicos.

Antes do PPR, você precisava escolher um modo para toda a rota. Se a tornasse estática, o carrinho estaria sempre desatualizado. Se a tornasse SSR, cada usuário pagaria o custo total da renderização no servidor — mesmo para conteúdo que não mudou desde o último deploy.O PPR permite que você tenha ambos, na mesma rota, na mesma resposta HTTP.

SSG / ISR

Static Generation

HTML at build time. CDN cached. Fastest TTFB possible. ✗ Stale user data. No personalization.

SSR

Server Rendering

Fresh data on every request. Full personalization. ✗ Slow TTFB. No CDN. Every user pays the cost.

Streaming SSR

Streaming

Sends chunks as they're ready. Progressive loading. ✗ Entire page is dynamic. No static CDN shell.

PPR · Next.js 16

Partial Prerendering

Static shell from CDN. Dynamic parts streamed in the same response. ✓ Edge TTFB + fresh data. Best of all strategies.

PPR is not a new rendering strategy. It's the end of the rendering strategy decision tree.

Como o PPR funciona nos bastidores

O modelo mental é mais simples do que parece. Uma regra, um limite, uma resposta.

Build time

O shell estático

At build time, Next.js renders everything it can — navigation, layout, headings, product descriptions, images, footers. Anything that doesn't depend on the incoming request. This becomes the static shell: a complete HTML document with Suspense fallback placeholders where the dynamic parts will go. The shell is stored at the CDN edge.

Request time

Os buracos dinâmicos

When a user requests the page, the CDN sends the cached shell immediately — that's your TTFB. In parallel, the origin server renders only the dynamic parts: the user's cart, their recommendations, real-time stock. These stream into the same HTTP response, filling the Suspense holes as they resolve. No second request. No client-side fetch. One response, two types of content.

The boundary

<Suspense> é a linha divisória

Everything outside a <Suspense> boundary is static — prerendered at build time, cached at the edge. Everything inside is dynamic — rendered at request time and streamed. The Suspense boundary is not just a loading UI: in PPR, it's the architectural boundary that separates static from dynamic.

Next.js 16 change

Dinâmico por padrão

In Next.js 16, all code is dynamic by default. You opt into static caching using the "use cache" directive or Suspense structure. This is the inverse of previous versions where pages were static by default. The experimental.ppr flag is gone — replaced by cacheComponents: true.

Habilitando o PPR no Next.js 16

Uma flag de configuração. Essa é toda a configuração necessária para desbloquear o Partial Prerendering para sua aplicação.

TYPESCRIPT
next.config.ts — enable Cache Components (Next.js 16)// next.config.ts — Next.js 16+import type { NextConfig } from 'next';const nextConfig: NextConfig = {  // Enables Partial Prerendering + Cache Components  cacheComponents: true,};export default nextConfig;// ⚠️ If you're on Next.js 14 or 15, use the old experimental flag:// const nextConfig = { experimental: { ppr: 'incremental' } }// The experimental.ppr flag was REMOVED in Next.js 16

Sua primeira página PPR — exemplo de listagem de produtos

TYPESCRIPT
app/products/[id]/page.tsx — static shell + dynamic holesimport { Suspense }            from 'react';import { ProductDetails }      from './product-details';   // staticimport { UserCart }            from './user-cart';          // dynamicimport { Recommendations }     from './recommendations';    // dynamicimport { CartSkeleton }        from './skeletons';import { RecommendationsSkeleton } from './skeletons';// This page uses PPR — shell is prerendered, holes are streamedexport default async function ProductPage({  params,}: {  params: { id: string };}) {  return (    <main>      {/*        ✅ STATIC SHELL        ProductDetails only uses params.id — known at build time        Next.js prerenders this and caches it at the CDN edge      */}      <ProductDetails productId={params.id} />      {/*        ✅ DYNAMIC HOLE #1 — reads cookies() inside        Wrapped in Suspense → streams at request time        CartSkeleton shows immediately while UserCart loads      */}      <Suspense fallback={<CartSkeleton />}>        <UserCart />      </Suspense>      {/*        ✅ DYNAMIC HOLE #2 — personalized per user        Streams in parallel with UserCart — no waterfall      */}      <Suspense fallback={<RecommendationsSkeleton />}>        <Recommendations productId={params.id} />      </Suspense>    </main>  );}
TYPESCRIPT
user-cart.tsx — dynamic component (uses request-time API)// app/products/[id]/user-cart.tsximport { cookies } from 'next/headers'; // ← makes this component dynamicexport async function UserCart() {  const sessionToken = (await cookies()).get('session')?.value;  if (!sessionToken) {    return <div>Sign in to see your cart</div>;  }  const cart = await fetchCart(sessionToken); // fetch user-specific data  return (    <div className="cart">      <span>{cart.itemCount} items</span>      <span>${cart.total}</span>    </div>  );}// Why is this dynamic?// cookies() accesses the incoming HTTP request — unknown at build time.// Any component that calls cookies(), headers(), or connection()// is automatically dynamic in Next.js 16.// PPR streams it in after the static shell is served.

A diretiva "use cache" — cache de granularidade fina

Além dos limites de Suspense, o Next.js 16 introduziu a "use cache" diretiva — uma maneira de marcar funções ou componentes individuais como armazenáveis em cache no nível do servidor.

TYPESCRIPT
"use cache" — component-level caching// Cache an individual async function — not the whole componentasync function getProductData(productId: string) {  'use cache'; // ← this function's result is cached  const data = await db.products.findUnique({ where: { id: productId } });  return data;}// Cache a component directlyasync function ProductHeader({ productId }: { productId: string }) {  'use cache'; // ← this component is cached at build time  const product = await getProductData(productId);  return (    <header>      <h1>{product.title}</h1>      <p>{product.description}</p>    </header>  );}// The key insight in Next.js 16:// "use cache" = explicit opt-in to static caching// No directive = dynamic by default// Suspense boundary = where dynamic content streams in//// Together, "use cache" + Suspense = Partial Prerendering

Lendo o output do build

O símbolo no output do build é como você confirma que o PPR está funcionando para uma rota.

TYPESCRIPT
Route (app)                Size    First Load JS/products/[id]            4.2 kB    89 kB/about                    1.1 kB    82 kB/blog/[slug]              2.3 kB    84 kBλ /dashboard                3.1 kB    86 kB○ Static    ● SSG    ◐ Partial Prerendering    λ Dynamic (SSR)

Migrando uma página SSR para PPR

A maioria das rotas SSR pode se tornar rotas PPR com mudanças mínimas. O segredo é identificar quais partes precisam de dados em tempo de requisição e envolvê-las em Suspense.

❌ Antes — SSR completo, TTFB lento
TYPESCRIPT
// The whole page waits for user data// Even the static header is blockedexport default async function Page() {  // ❌ All of this blocks the entire page  const user    = await getUser();      // auth check  const cart    = await getCart(user);  // user-specific  const recs    = await getRecs(user);  // personalized  const product = await getProduct();   // static data  // Nothing ships until ALL of these resolve  return (    <main>      <Header />      <ProductDetails product={product} />      <CartWidget    cart={cart} />      <Recommendations recs={recs} />    </main>  );}
✅ Depois — PPR, TTFB na borda + dados atualizados
TYPESCRIPT
// Static content ships immediately from CDN// Dynamic content streams in parallelexport default async function Page({ params }) {  // ✅ Only static data fetched at the page level  const product = await getProduct(params.id);  return (    <main>      {/* ✅ Static — in the prerendered shell /}      <Header />      <ProductDetails product={product} />      {/ ✅ Dynamic — streamed at request time /}      <Suspense fallback={<CartSkeleton />}>        <CartWidget />        {/ fetches user + cart inside /}      </Suspense>      <Suspense fallback={<RecsSkeleton />}>        <Recommendations id={params.id} /> {/ personalized */}      </Suspense>    </main>  );}

Onde o PPR falha em produção

O PPR não é uma bala de prata. Existe uma categoria de conteúdo onde o PPR pode, na verdade, prejudicá-lo — e é fácil deixar passar despercebido durante o desenvolvimento.

Tipo de conteúdo

Colocar no shell estático

Manter no Suspense (dinâmico)

Título do produto, descrição, imagens

✓ Shell estático

Navegação, layout, rodapé

✓ Shell estático

Conteúdo de blog, documentação

✓ Shell estático

Carrinho do usuário, lista de desejos

✗ Nunca estático

✓ Transmitir via Suspense

Recomendações personalizadas

✗ Nunca estático

✓ Transmitir via Suspense

Preço do produto

✗ Evitar — risco de desatualização

✓ Transmitir se mudar

Contagem de estoque em tempo real

✗ Nunca estático

✓ Transmitir via Suspense

Estado de autenticação

✗ Nunca estático

✓ Transmitir via Suspense

Variante de teste A/B

⚠ Apenas se tolerar desatualização

✓ Preferir Suspense

PPR, SSR, SSG, ISR — a matriz de estratégia

Essas não são estratégias concorrentes. Elas são camadas. O PPR funciona melhor quando 60%+ da sua página é estática.

Estratégia

TTFB

Dados atualizados

Personalizado

Usar quando

SSG

Velocidade na borda

Apenas no tempo de build

Não

Páginas de marketing, docs — zero conteúdo dinâmico

ISR

Velocidade na borda

Revalidado

Não

Blog, catálogo — muda previsivelmente em um cronograma

PPR

Velocidade na borda

Por requisição

Sim (transmitido)

60%+ layout estático + dados dinâmicos do usuário — e-commerce, dashboards com shell público

SSR

Latência da origem

Por requisição

Sim

Cada byte é específico do usuário — painéis administrativos, dashboards financeiros

CSR

Shell instantâneo

Fetch do cliente

Sim

UI altamente interativa atrás de autenticação — mapas, editores, apps complexos

✓ PPR é a escolha certa quando
  • - Páginas de produto: conteúdo estático + barra lateral personalizada, carrinho, recomendações

  • - Artigos de notícias ou blog com comentários, anúncios específicos do usuário ou progresso de leitura

  • - Landing pages com elementos testados em A/B ou CTAs específicos do usuário

  • - Dashboard com um shell público e widgets específicos do usuário

  • - 60%+ do conteúdo da página não muda por usuário

  • SEO importa — o shell estático é imediatamente rastreável

→ Escolha uma estratégia diferente quando
  • - Cada byte é específico do usuário (dados financeiros, painéis administrativos) → SSR

  • - O conteúdo é 100% estático e nunca personaliza → SSG ou ISR

  • - Preços ou estoque estão na área acima da dobra (risco de desatualização) → SSR

  • - Você está no Pages Router — o PPR requer o App Router

  • - Você precisa de consistência total entre o shell e o conteúdo dinâmico o tempo todo

v14
Out 2023 — Anunciado na Next.js Conf.
Introduced as experimental behind experimental.ppr: true. The 'incremental' mode allowed per-route opt-in via export const experimental_ppr = true. Not stable — API subject to change.
v15
Out 2024 — Ainda experimental.
Same flags, refined behavior. Vercel continued positioning PPR as not production-ready for most apps. The incremental opt-in remained the recommended way to test per route.
v16
Out 2025 — PPR graduado para estável via Cache Components.
experimental.ppr flag is removed. New model: cacheComponents: true in next.config.ts. The "use cache" directive replaces the explicit experimental_ppr route export. All code is now dynamic by default. React Compiler support promoted to stable.

O que o PPR significa para como você constrói apps Next.js

O PPR não substitui SSR, SSG, ISR ou CSR. Ele coordena com eles. A árvore de decisão de renderização que definiu o desenvolvimento no Next.js por anos — "esta página é estática ou dinâmica?" — agora é uma decisão de nível de componente, não de nível de rota.

A mudança arquitetural é real: no Next.js 16, você para de pensar nas páginas como tendo um único modo de renderização. Você começa a pensar em quais partes desta página são estáticas e quais partes precisam ser atualizadas. Suspense é seu limite. O lado estático vai para a borda da CDN e é enviado com latência de borda. O lado dinâmico é transmitido da origem na mesma resposta HTTP.

O resultado prático é que a maioria dos sites de conteúdo, páginas de e-commerce e dashboards com shell público e dados específicos do usuário tornam-se mais rápidos sem qualquer custo de SSR. A página que seu usuário vê é o mesmo dado — apenas chega em uma ordem mais inteligente.

O único ponto para ter cuidado: nunca coloque conteúdo no shell estático sobre o qual os usuários possam agir antes que a atualização dinâmica chegue. Preços desatualizados, estoque desatualizado, descontos desatualizados no shell enquanto a transmissão ainda está carregando — é aí que o PPR pode prejudicar a conversão em vez de ajudar.

Fontes — Junho 2026

  • nextjs.org/blog/next-16 (Out 2025) — Notas de lançamento oficiais do Next.js 16: Cache Components, PPR estável, cacheComponents: true, React Compiler estável, Turbopack estável.

  • nextjs.org/docs/app/guides/ppr-platform-guide (Mar 2026) — Guia de plataforma: shell estático, postponedState, protocolo de resumo de CDN, arquitetura de streaming.

  • nextjs.org/docs/15/app/getting-started/partial-prerendering (Ago 2025) — Guia de introdução: experimental_ppr, limites de Suspense, "use cache" diretiva.

  • PageGlass (27 de abr de 2026) — "Partial Prerendering: Um Guia Prático (2026)" — PPR vs streaming SSR, cache de CDN, requisito do App Router, regra 60/30.

  • TheCodeForge (11 de abr de 2026) — "Partial Prerendering no Next.js 16" — armadilhas de produção, estudo de caso de queda de 12% na conversão, matriz de estratégia, composição PPR + ISR.

  • DEV Community / Pockit (18 de mar de 2026) — "Mergulho Profundo no PPR do Next.js" — indicador de output de build, depuração de páginas totalmente dinâmicas, padrões de migração.

  • Ashish Gogula (4 de dez de 2025) — "Um Guia Prático para PPR no Next.js 16" — cacheComponents: true config, projeto de demonstração, padrão Suspense + esqueleto.

  • U11D (13 de fev de 2026) — "Next.js 16 PPR: O Melhor da Renderização Estática e Dinâmica" — caso de uso de e-commerce, "use cache" diretiva, comparação com SSG/ISR.


Exemplos de código refletem a API estável do Next.js 16 (outubro de 2025). A flag experimental.ppr e o experimental_ppr export de rota foram removidos no Next.js 16 — não os use em novos projetos. cacheComponents: true é a API estável. O PPR requer o App Router — não está disponível no Pages Router. Sempre verifique o comportamento com seu provedor de CDN, pois o comportamento de cache na borda para rotas PPR varia por plataforma.