Partial Prerendering (PPR)
Sirva um shell estático instantaneamente e transmita conteúdo dinâmico para buracos do Suspense -- o melhor do renderização estática e dinâmica.
Busque em todas as páginas da documentação
Sirva um shell estático instantaneamente e transmita conteúdo dinâmico para buracos do Suspense -- o melhor do renderização estática e dinâmica.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida -- pronto para copiar e colar.
// next.config.ts -- habilita PPR
import type { NextConfig } from "next";
const config: NextConfig = {
experimental: {
ppr: true, // ou "incremental" para adesão por rota
},
};
export default config;// app/page.tsx -- shell estático com buracos dinâmicos
import { Suspense } from "react";
import { cookies } from "next/headers";
export default function Page() {
return (
<main>
<h1>Bem-vindo à Loja</h1> {/* Shell estático */}
<StaticHero /> {/* Shell estático */}
<Suspense fallback={<CartSkeleton />}>
<DynamicCart /> {/* Buraco dinâmico -- transmitido */}
</Suspense>
</main>
);
}
async function DynamicCart() {
const cookieStore = await cookies();
const cartId = cookieStore.get("cart-id")?.value;
const cart = await fetchCart(cartId);
return <CartWidget items={cart.items} />;
}Quando usar isso: Você tem uma página onde a maior parte do conteúdo é estática (informações do produto, texto de marketing), mas uma pequena parte depende da requisição (sessão do usuário, carrinho, personalização).
// next.config.ts
import type { NextConfig } from "next";
const config: NextConfig = {
experimental: {
ppr: "incremental", // adesão por rota
},
};
export default config;// app/products/[slug]/page.tsx
import { Suspense } from "react";
import { cookies } from "next/headers";
import { db } from "@/lib/db";
import { AddToCartButton } from "./add-to-cart";
// Pré-gera esta rota para PPR
export const experimental_ppr = true;
// Pré-gera páginas de produto no momento da compilação
export async function generateStaticParams() {
const products = await db.product.findMany({ select: { slug: true } });
return products.map((p) => ({ slug: p.slug }));
}
type Props = { params: Promise<{ slug: string }> };
export default async function ProductPage({ params }: Props) {
const { slug } = await params;
const product = await db.product.findUnique({ where: { slug } });
if (!product) return <p>Produto não encontrado</p>;
return (
<main className="max-w-4xl mx-auto p-6">
{/* ---- Shell Estático (pré-gerado no momento da compilação) ---- */}
<div className="grid grid-cols-2 gap-8">
<img
src={product.imageUrl}
alt={product.name}
className="w-full rounded"
/>
<div>
<h1 className="text-3xl font-bold">{product.name}</h1>
<p className="text-2xl text-gray-700 mt-2">
${product.price.toFixed(2)}
</p>
<p className="mt-4 text-gray-600">{product.description}</p>
<AddToCartButton productId={product.id} />
</div>
</div>
{/* ---- Buracos Dinâmicos (transmitidos no momento da requisição) ---- */}
<section className="mt-12">
<h2 className="text-xl font-bold mb-4">Personalizado para Você</h2>
<Suspense fallback={<RecommendationsSkeleton />}>
<PersonalizedRecommendations />
</Suspense>
</section>
<section className="mt-8">
<h2 className="text-xl font-bold mb-4">Suas Visualizações Recentes</h2>
<Suspense fallback={<RecentViewsSkeleton />}>
<RecentlyViewed />
</Suspense>
</section>
</main>
);
}
// ---- Componentes Dinâmicos (usam dados do momento da requisição) ----
async function PersonalizedRecommendations() {
const cookieStore = await cookies();
const userId = cookieStore.get("user-id")?.value;
if (!userId) return <p>Faça login para recomendações personalizadas</p>;
const recs = await fetch(
`https://api.example.com/recommendations?user=${userId}`,
{ cache: "no-store" }
);
const products = await recs.json();
return (
<div className="grid grid-cols-4 gap-4">
{products.map((p: { id: string; name: string; price: number }) => (
<div key={p.id} className="border rounded p-3">
<p className="font-medium">{p.name}</p>
<p className="text-gray-500">${p.price.toFixed(2)}</p>
</div>
))}
</div>
);
}
async function RecentlyViewed() {
const cookieStore = await cookies();
const history = cookieStore.get("view-history")?.value;
if (!history) return <p>Nenhuma visualização recente</p>;
const ids = JSON.parse(history) as string[];
const products = await db.product.findMany({
where: { id: { in: ids } },
});
return (
<div className="flex gap-4 overflow-x-auto">
{products.map((p) => (
<div key={p.id} className="min-w-[150px] border rounded p-3">
<p className="font-medium">{p.name}</p>
</div>
))}
</div>
);
}
// ---- Esqueletos ----
function RecommendationsSkeleton() {
return (
<div className="grid grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="h-24 bg-gray-100 rounded animate-pulse" />
))}
</div>
);
}
function RecentViewsSkeleton() {
return (
<div className="flex gap-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="w-[150px] h-20 bg-gray-100 rounded animate-pulse" />
))}
</div>
);
}O que isso demonstra:
experimental_ppr = truecookies() e são transmitidos no momento da requisiçãogenerateStaticParams pré-gera páginas de produto no momento da compilaçãocookies(), headers(), searchParams, ou cache: "no-store").Adoção incremental (adesão por rota):
// next.config.ts
export default { experimental: { ppr: "incremental" } };
// Apenas esta rota usa PPR
// app/products/[slug]/page.tsx
export const experimental_ppr = true;PPR em todas as rotas (global):
// next.config.ts
export default { experimental: { ppr: true } };
// Todas as rotas usam PPR automaticamenteMúltiplos buracos dinâmicos com fontes de dados independentes:
export default function Page() {
return (
<main>
<StaticNav />
<StaticHero />
{/* Cada limite do Suspense é um buraco dinâmico independente */}
<Suspense fallback={<UserBarSkeleton />}>
<UserBar /> {/* cookies() */}
</Suspense>
<Suspense fallback={<PricingSkeleton />}>
<LivePricing /> {/* cache: "no-store" */}
</Suspense>
<Suspense fallback={<WeatherSkeleton />}>
<LocalWeather /> {/* headers() para geo */}
</Suspense>
<StaticFooter />
</main>
);
}// Exportação de adesão por rota para PPR
export const experimental_ppr: boolean = true;
// Tipo do next.config.ts
import type { NextConfig } from "next";
const config: NextConfig = {
experimental: {
ppr: true | "incremental",
},
};PPR é experimental -- A partir do Next.js 15/16, PPR requer a flag experimental.ppr. A API pode mudar. Correção: Fixe sua versão do Next.js e teste completamente antes de usar em produção.
A colocação do limite do Suspense determina a divisão -- Se você esquecer de envolver um componente dinâmico em Suspense, a página inteira se tornará dinâmica. Correção: Sempre envolva componentes que usam funções dinâmicas em um limite <Suspense>.
A qualidade do fallback é importante -- O fallback do Suspense faz parte do shell estático e é visível para todos os usuários instantaneamente. Um fallback ruim cria uma má primeira impressão. Correção: Use esqueletos bem projetados que correspondam à forma e ao tamanho do conteúdo real.
Buracos dinâmicos adicionam custo ao servidor -- Cada buraco dinâmico requer computação do servidor no momento da requisição. Muitos buracos dinâmicos anulam os benefícios de CDN. Correção: Consolide dados dinâmicos relacionados em menos limites do Suspense.
Sem PPR no Edge Runtime para todos os provedores -- PPR requer que a plataforma de hospedagem suporte streaming e a API postpone. Correção: Verifique se sua plataforma de implantação suporta PPR (Vercel suporta nativamente).
Componentes do Cliente no shell estático ainda enviam JavaScript -- PPR torna o HTML estático, mas os Componentes do Cliente ainda hidratam no cliente. Correção: Mantenha o shell estático composto principalmente de Componentes do Servidor para JavaScript mínimo.
| Abordagem | Usar Quando | Não Usar Quando |
|---|---|---|
| PPR | A maior parte da página é estática, mas algumas partes precisam de dados do momento da requisição | A página inteira é totalmente estática ou totalmente dinâmica |
| Geração Estática Completa | A página inteira pode ser construída no momento da implantação | Qualquer parte precisa de dados específicos do usuário |
| Renderização Dinâmica Completa | A página inteira depende de dados do momento da requisição | A maior parte do conteúdo é o mesmo para todos os usuários |
| Fetching no lado do cliente | Dados dinâmicos devem carregar após a hidratação, não bloquear SSR | Você quer os dados dinâmicos no HTML inicial |
| ISR | O conteúdo muda periodicamente, mas não por requisição | O conteúdo é personalizado por usuário |
| Streaming SSR (sem PPR) | Você quer renderização progressiva sem um shell estático | Você quer o shell servido da borda do CDN |
PPR combina geração estática e renderização dinâmica em uma única rota. As partes estáticas são pré-geradas no momento da compilação e servidas do CDN. As partes dinâmicas são transmitidas para limites do Suspense no momento da requisição.
Adicione experimental: { ppr: true } (global) ou experimental: { ppr: "incremental" } (adesão por rota) ao next.config.ts. Para o modo incremental, também exporte experimental_ppr = true no arquivo da página.
Limites do Suspense definem a divisão. Tudo fora de um limite do Suspense faz parte do shell estático. Componentes dentro de um limite do Suspense que chamam funções dinâmicas (cookies(), headers(), cache: "no-store") se tornam buracos dinâmicos.
O fallback faz parte do shell estático -- ele é pré-gerado e servido instantaneamente do CDN. Os usuários veem o esqueleto imediatamente enquanto o conteúdo dinâmico é transmitido.
A página inteira se torna dinâmica. Sem um limite do Suspense, o PPR não consegue identificar a divisão estática/dinâmica, então toda a rota volta para renderização dinâmica completa.
Sim. PPR torna o HTML estático, mas os Componentes do Cliente ainda hidratam no cliente e enviam seu JavaScript. Mantenha o shell estático composto principalmente de Componentes do Servidor para JS mínimo.
Sim. Cada limite do Suspense é um buraco dinâmico independente com sua própria fonte de dados:
<Suspense fallback={<UserBarSkeleton />}>
<UserBar /> {/* cookies() */}
</Suspense>
<Suspense fallback={<PricingSkeleton />}>
<LivePricing /> {/* cache: "no-store" */}
</Suspense>// next.config.ts
export default { experimental: { ppr: "incremental" } };
// app/products/[slug]/page.tsx
export const experimental_ppr = true;export const experimental_ppr: boolean = true;No next.config.ts, a opção PPR é tipada como true | "incremental" dentro do objeto experimental de NextConfig.
Sim. generateStaticParams pré-gera páginas de produto no momento da compilação como o shell estático. Buracos dinâmicos dentro dessas páginas são transmitidos no momento da requisição.
PPR requer uma plataforma de hospedagem que suporte streaming e a API postpone. Vercel suporta nativamente. Verifique sua plataforma de implantação antes de habilitar PPR em produção.
Cada buraco dinâmico requer computação do servidor no momento da requisição. Muitos buracos dinâmicos anulam os benefícios de desempenho do CDN do shell estático. Consolide dados dinâmicos relacionados em menos limites do Suspense, quando possível.
Revisado por Chris St. John·Última atualização: 7 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥