A lo largo de la historia del desarrollo web, los ingenieros se vieron obligados a asumir un compromiso arquitectónico entre la generación estática (SSG) y el renderizado dinámico en servidor (SSR):
Tratar de solucionar este dilema en el lado del cliente (enviar un esqueleto estático y luego ejecutar un fetch en el useEffect) deteriora gravemente las métricas Core Web Vitals: saltos de diseño (CLS), latencia de interactividad (INP) y cascadas de red en el cliente.
Partial Prerendering (PPR) suprime esta disyuntiva desde los cimientos arquitectónicos. PPR compila la estructura estática de la página (Layouts, navegación, pie de página, esqueletos de reserva) durante la compilación del proyecto, mientras renderiza en el servidor las zonas dinámicas al recibir la petición y las transmite al navegador mediante streaming HTTP continuo sobre la misma conexión de red.
Partial Prerendering segmenta el ciclo de vida de la página en dos fases de ejecución independientes:
Fase de pre-renderizado en compilación (Build-time Prerender Pass):
El compilador de Next.js recorre el árbol de componentes durante next build. Las secciones estáticas se compilan en una estructura inmutable: el Static Shell. Cuando el renderizador topa con un límite <Suspense> que aloja llamadas a APIs dinámicas (await cookies(), await headers(), acceso a searchParams), el motor invoca la React Postpone API. La ejecución de esa rama dinámica se congela, se incrusta el esqueleto fallback en el Static Shell y se genera un marcador de Dynamic Hole (Ranura dinámica). El Static Shell generado se despliega en las cachés de Edge CDN.
Fase de reanudación en petición (Request-time Resume Pass): Cuando un usuario solicita la URL:
FASE 1: COMPILACIÓN DEL PROYECTO (next build / Build-time Prerender Pass)
─────────────────────────────────────────────────────────────────────────────
El compilador recorre el árbol de la ruta:
[Root Layout] ──► [Navigation] ──► [Static Products] ──► [<Suspense>]
│
¡Se detecta cookies()!
│
React.postpone() ──┐
▼
[Guardar en Static Shell (HTML + RSC)] ◄── [Insertar CartSkeleton fallback]
│
▼
Subida a la caché de Edge CDN
FASE 2: PROCESAMIENTO DE PETICIÓN (Request-time Resume Pass)
─────────────────────────────────────────────────────────────────────────────
Navegador Edge CDN Serverless Worker (Origen)
│ │ │
(1)├─── GET /shop ──────────►│ │
│ ├── Envío inmediato de Shell(TTFB)─►│
│◄─── Chunk 1: Static Shell┤ [¡El usuario percibe FCP en 30ms!]│
│ (con CartSkeleton) │ │
│ │ Reanudación desde postpone:
│ │ const cookie = await cookies();
│ │ const cart = await fetchCart();
│ │ │
(2)│ │◄── Chunk 2: Ranura dinámica ──────┤
│◄── Sustituir Skeleton ──┤ HTML del carrito + $RC() │
│ por contenido vivo │ │Next.js ensambla la estructura estática. Al encontrar APIs dinámicas dentro de Suspense, suspende la rama con React.postpone() y guarda el esqueleto fallback.
El usuario abre la página. Edge CDN envía de inmediato el Static Shell en caché. El navegador dibuja la interfaz en 20-30 ms.
El servidor de origen reanuda la ejecución únicamente en las ranuras dinámicas, leyendo las cookies reales y ejecutando consultas a la BD.
El HTML dinámico generado y los RSC Flight Payloads se canalizan por la conexión HTTP abierta sin necesidad de recargar la página.
Un script en línea de React intercambia el esqueleto por el contenido final, dando paso a la hidratación selectiva de componentes interactivos.
¿Qué sucede durante la compilación estática (next build) si un componente en una ruta con experimental_ppr = true invoca await cookies() SIN envolverse en un límite <Suspense>?
next.config.ts#En las versiones modernas de Next.js, Partial Prerendering se activa a través del bloque de configuración experimental. La opción 'incremental' permite adoptar PPR de manera gradual en rutas puntuales:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
// 'incremental' habilita PPR por segmento mediante export const experimental_ppr = true
ppr: 'incremental',
},
};
export default nextConfig;Para habilitar Partial Prerendering en una página o layout determinado, exporta la constante experimental_ppr:
import { Suspense } from 'react';
import { StaticCatalogHeader } from '@/components/StaticCatalogHeader';
import { StaticProductGrid } from '@/components/StaticProductGrid';
import { PersonalizedCartWidget } from '@/components/PersonalizedCartWidget';
import { CartSkeleton } from '@/components/skeletons';
// Activamos Partial Prerendering para esta ruta específica
export const experimental_ppr = true;
export default function CatalogPage() {
return (
<main className="max-w-7xl mx-auto p-6 space-y-8">
{/* Sección estática: compilada en tiempo de build (Build-time Prerender) */}
<StaticCatalogHeader />
<div className="flex justify-between items-start gap-8">
{/* Catálogo estático de productos: servido desde Edge CDN de forma instantánea */}
<div className="flex-1">
<StaticProductGrid />
</div>
{/* El límite Suspense define la ranura dinámica (Dynamic Hole) */}
<aside className="w-80">
<Suspense fallback={<CartSkeleton />}>
{/* Este componente lee cookies() y se computa durante la petición del usuario */}
<PersonalizedCartWidget />
</Suspense>
</aside>
</div>
</main>
);
}Un componente se detecta automáticamente como ranura dinámica si dentro de su árbol de ejecución se invoca:
await cookies() de 'next/headers' (asíncrono en Next.js 15+)await headers() de 'next/headers' (asíncrono en Next.js 15+)searchParams de la página (params: Promise<...>, searchParams: Promise<...>)await connection() de 'next/server' (desencadenante explícito de streaming diferido)fetch(..., { cache: 'no-store' })En una tienda en línea, el catálogo de productos es idéntico para todos los clientes, pero el widget del carrito en la cabecera depende de la cookie de sesión del usuario:
import { cookies } from 'next/headers';
import { db } from '@/lib/db';
export async function PersonalizedCartWidget() {
// ¡En Next.js 15+, la invocación de cookies() es estrictamente asíncrona!
const cookieStore = await cookies();
const sessionToken = cookieStore.get('session_token')?.value;
if (!sessionToken) {
return <div className="text-sm text-text-muted">El carrito está vacío</div>;
}
// La consulta se ejecuta exclusivamente ante la petición del usuario
const cart = await db.cart.findUnique({
where: { sessionToken },
include: { items: true },
});
const count = cart?.items.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
return (
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg border border-border bg-bg-elevated">
<span className="text-sm font-medium text-text-primary">Artículos en cesta:</span>
<span className="font-mono font-bold text-accent-cyan">{count}</span>
</div>
);
}connection() en Next.js 15+#Cuando un componente no requiere cookies pero ejecuta cálculos analíticos pesados o consulta cotizaciones en vivo que no deben fijarse en build time, utiliza la utilidad connection():
import { connection } from 'next/server';
import { fetchLiveExchangeRates } from '@/lib/finance';
export async function LiveStockTicker() {
// ¡await connection() ejecuta explícitamente React.postpone() en build time!
// Esto indica al compilador: no intentes evaluar este código durante la compilación,
// ejecútalo estrictamente cuando se establezca la conexión real del cliente.
await connection();
const rates = await fetchLiveExchangeRates();
return (
<div className="flex gap-4 text-xs font-mono py-1 overflow-x-auto text-text-muted">
<span>USD/EUR: {rates.USDEUR}</span>
<span>GBP/EUR: {rates.GBPEUR}</span>
<span>BTC/USD: {rates.BTCUSD}</span>
</div>
);
}¿Qué ejecuta el servidor de origen cuando recibe una petición para una página con PPR cuyo Static Shell ya reside en la caché de Edge CDN?
Si bajo el modo PPR ejecutas una función asíncrona dinámica (await cookies(), await headers(), acceso a searchParams) dentro de un Server Component que no está encapsulado en un <Suspense>, Next.js no podrá generar el Static Shell y abortará next build con un error fatal.
Solución: Cualquier componente que acceda al contexto dinámico de la petición debe aislarse dentro de <Suspense fallback={<Skeleton />}>.
Error: Route /shop with `experimental_ppr` enabled accessed dynamic data without a Suspense boundary.Los layouts raíz (layout.tsx) en Next.js no tienen acceso deliberadamente a la prop searchParams. Los layouts son persistentes y no se re-renderizan con los cambios de parámetros de consulta, lo que permite almacenarlos de forma permanente en el Static Shell.
Solución: Lee searchParams únicamente dentro de page.tsx o en un Client Component mediante el hook useSearchParams().
Dado que una respuesta en PPR comienza con un fragmento estático de CDN y culmina con un flujo dinámico del servidor, proxies corporativos mal configurados o instancias Nginx sin ajustar podrían intentar cachear la respuesta íntegra (¡incluyendo datos privados del carrito del usuario emitidos en el streaming!).
Solución: Al auto-hospedar mediante Docker o Nginx, verifica que los proxies inversos soporten Transfer-Encoding: chunked y no almacenen en búfer respuestas marcadas con Cache-Control: private, no-cache, no-store.
Envolver decenas de cadenas de texto microscópicas en límites Suspense individuales fragmenta la respuesta HTTP en cientos de paquetes diminutos, elevando la sobrecarga de TCP y TLS. Agrupa los bloques dinámicos relacionados en contenedores lógicos comunes.