{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering","title":"Next.js 15 PPR: Partial Prerendering, Static Shells, y el Fin del Debate ISR vs SSR","description":"Cómo el Partial Prerendering de Next.js 15 combina rendering estático y dinámico en un solo request. Static shell + dynamic holes vía Suspense, mediciones reales de TTFB, y guía práctica de migración desde ISR.","language":"es","datePublished":"2025-12-17","dateModified":null,"contentFormat":"text/markdown","content":"Frontend\n\n# Next.js 15 PPR: Partial Prerendering, Static Shells, y el Fin del Debate ISR vs SSR\n\n2025-12-175 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/next-js-15-ppr-partial-prerendering>)\n\n## El Problema que PPR Resuelve\n\nConsidérese una página típica de producto de e-commerce:\n\n```\n┌──────────────────────────────────────┐\n│ Header (con avatar de usuario) │ ← Dinámico (necesita auth)\n├──────────────────────────────────────┤\n│ Galería de Imágenes del Producto │ ← Estático (cambia mensualmente)\n│ Título y Descripción del Producto │ ← Estático\n│ Reviews del Producto (234 reviews) │ ← Estático (cacheado)\n├──────────────────────────────────────┤\n│ Precio: $49.99 │ ← Dinámico (cambia cada hora)\n│ Stock: quedan 3 │ ← Dinámico (tiempo real)\n│ [Agregar al Carrito] │ ← Dinámico (necesita sesión)\n├──────────────────────────────────────┤\n│ Productos Relacionados │ ← Estático\n│ Footer │ ← Estático\n└──────────────────────────────────────┘\n```\n\nPPR introduce una opción nueva: **servir las partes estáticas instantáneamente desde el CDN, y streamear las partes dinámicas desde el origin en la misma respuesta.**\n\n## Cómo Funciona PPR\n\nPPR en [Next.js](<https://nextjs.org/>) usa boundaries de React Suspense para definir la separación entre contenido estático y dinámico:\n\n```jsx\n// app/products/[slug]/page.jsx\nimport { Suspense } from 'react';\n\nexport default async function ProductPage({ params }) {\n const product = await getProduct(params.slug); // datos estáticos\n\n return (\n <div className=\"product-page\">\n {/* Shell estático, prerenderizado en build time */}\n <ProductDetails product={product} />\n <ProductReviews productId={product.id} />\n\n {/* Agujeros dinámicos, streameados en request time */}\n <Suspense fallback={<PriceSkeleton />}>\n <PricingWidget productId={product.id} />\n </Suspense>\n\n <Suspense fallback={<CartSkeleton />}>\n <CartButton productId={product.id} />\n </Suspense>\n </div>\n );\n}\n```\n\nCuando un usuario pide esta página:\n\n```\nTimeline:\n\n0ms: CDN recibe request\n5ms: CDN sirve shell estático (HTML prerenderizado)\n Usuario ve: imágenes, descripción, reviews, skeletons de carga\n5ms: CDN simultáneamente reenvía request al origin para agujeros dinámicos\n200ms: Origin envía contenido dinámico (precio, stock, botón de carrito)\n Usuario ve: página completa con precio y stock en tiempo real\n```\n\nEl TTFB es 5ms (el shell estático desde CDN), no 200ms (la respuesta SSR completa).\n\n## Habilitando PPR\n\n```javascript\n// next.config.js\n/** @type {import('next').NextConfig} */\nconst nextConfig = {\n experimental: {\n ppr: true,\n },\n};\n\nmodule.exports = nextConfig;\n```\n\nO por ruta:\n\n```javascript\n// app/products/[slug]/page.jsx\nexport const experimental_ppr = true;\n```\n\n## ¿Cómo Sabe Next.js Qué es Dinámico?\n\nDetecta uso de APIs dinámicas:\n\n```javascript\n// Estos hacen un componente dinámico:\ncookies() // Lee cookies del request\nheaders() // Lee headers del request\nsearchParams // Prop de page desde URL search params\nunstable_noStore() // Opt-out explícito de caching\n\n// Estos siguen siendo estáticos (cacheados en build):\nfetch('https://api.example.com/product/123') // Cacheado por default\n```\n\n## Mediciones Reales de TTFB\n\nEstas mediciones son de una página de listado de productos con 20 productos, cada uno con precio y stock dinámicos:\n\n```\nEstrategia de Rendering TTFB (p50) TTFB (p99) Carga Total\n──────────────────────────────────────────────────────────────────\nSSR (sin cache) 380ms 1200ms 920ms\nISR (revalidación 60s) 45ms 120ms 520ms\nISR + client fetch 45ms 120ms 780ms*\nPPR 42ms 95ms 580ms\n──────────────────────────────────────────────────────────────────\n* ISR + client fetch tiene layout shift cuando cargan los precios\n```\n\nLa ganancia real es la experiencia de usuario. Con ISR + client fetch, la página carga, el usuario ve precios placeholder, después 300ms más tarde los precios reales aparecen con layout shift. Con PPR, el skeleton aparece en el shell estático y se reemplaza seamlessly por el contenido streameado, sin layout shift, sin flash de contenido incorrecto.\n\n## Migración desde ISR\n\n### Antes (ISR)\n\n```javascript\nexport const revalidate = 60;\n\nexport default async function ProductPage({ params }) {\n const product = await getProduct(params.slug);\n const price = await getPrice(product.id); // Stale por hasta 60s\n const stock = await getStock(product.id); // Stale por hasta 60s\n\n return (\n <div>\n <ProductDetails product={product} />\n <div className=\"price\">{price}</div>\n <div className=\"stock\">{stock} restantes</div>\n <AddToCartButton productId={product.id} />\n </div>\n );\n}\n```\n\n### Después (PPR)\n\n```javascript\nexport const experimental_ppr = true;\n\nexport default async function ProductPage({ params }) {\n const product = await getProduct(params.slug);\n\n return (\n <div>\n <ProductDetails product={product} />\n\n <Suspense fallback={<div className=\"price-skeleton\" />}>\n <DynamicPricing productId={product.id} />\n </Suspense>\n\n <Suspense fallback={<div className=\"stock-skeleton\" />}>\n <DynamicStock productId={product.id} />\n </Suspense>\n\n <Suspense fallback={<div className=\"cart-skeleton\" />}>\n <AddToCartButton productId={product.id} />\n </Suspense>\n </div>\n );\n}\n\nimport { unstable_noStore } from 'next/cache';\n\nasync function DynamicPricing({ productId }) {\n unstable_noStore();\n const price = await getPrice(productId);\n return <div className=\"price\">{formatPrice(price)}</div>;\n}\n\nasync function DynamicStock({ productId }) {\n unstable_noStore();\n const stock = await getStock(productId);\n return (\n <div className={`stock ${stock < 5 ? 'low-stock' : ''}`}>\n {stock > 0 ? `${stock} restantes` : 'Sin stock'}\n </div>\n );\n}\n```\n\n## Errores Comunes\n\n**Datos dinámicos en layouts se propagan.** Si tu root layout lee ` cookies() ` para preferencia de tema, todo el sitio se vuelve dinámico. Mové la lectura de cookies a un boundary de Suspense dentro del layout.\n\n**El diseño de skeletons importa.** El shell estático es lo que los usuarios ven los primeros 100-300ms. Si tus skeletons no matchean las dimensiones del layout final, teners Cumulative Layout Shift.\n\n```css\n.price-skeleton {\n height: 32px;\n width: 120px;\n background: var(--skeleton-bg);\n border-radius: 4px;\n animation: pulse 1.5s ease-in-out infinite;\n}\n```\n\n**No todo necesita ser dinámico.** Un error común es wrappear demasiadas cosas en Suspense. La mayoría de los datos cambian infrecuentemente y deberían quedarse en el shell estático con revalidación estilo ISR.\n\n## Cuándo NO Usar PPR\n\n- **Páginas completamente estáticas** (blog, docs): Utilizar SSG regular.\n- **Páginas completamente dinámicas** (dashboards, paneles admin): Utilizar SSR regular.\n- **Páginas con contenido altamente personalizado**: Si el 80%+ de la página es user-specific, el shell estático es demasiado fino para proveer mejora significativa de TTFB.\n\nPPR brilla cuando teners páginas mayormente estáticas con algunos widgets dinámicos, e-commerce, sitios de noticias, páginas de marketing con stats en tiempo real, catálogos de productos con precios en vivo. Eso cubre un porcentaje sorprendentemente grande de aplicaciones web en producción.\n\nEl debate ISR vs SSR terminó. PPR no es un compromiso entre los dos, es un modelo de rendering genuinamente nuevo que te da el TTFB de generación estática con la frescura de rendering server-side. La clave es pensar la página como un shell estático con agujeros dinámicos en vez de un render monolítico. Una vez que ese modelo mental hace click, la implementación fluye naturalmente.\n\n## Preguntas frecuentes\n\n### ¿Cuál es la diferencia entre PPR e ISR?\n\nISR (Incremental Static Regeneration) sirve una página completamente cacheada y revalida en background después de un timeout. La página entera es stale o fresh. PPR sirve un shell estático inmediatamente (desde el CDN edge) y streamea contenido dinámico en la misma respuesta. Diferentes partes de la página pueden tener diferentes garantías de frescura.\n\n### ¿PPR funciona con el Pages Router?\n\nNo. PPR requiere el App Router porque depende de React Server Components y boundaries de Suspense para definir el split estático/dinámico. Si se está en el Pages Router, ISR con getStaticProps sigue siendo tu mejor opción para páginas híbridas estáticas/dinámicas.\n\n### ¿Cuánto mejora el TTFB con PPR?\n\nEn las mediciones, PPR mejora el TTFB un 60-80% comparado con SSR completo porque el shell estático se sirve desde el CDN edge sin tocar el servidor de origen. La mejora en tiempo total de carga es menor (20-40%) porque el contenido dinámico todavía necesita computarse y streamearse. La mejora percibida es aún mayor porque los usuarios ven el shell estático inmediatamente.\n\n## Tips\n\n1. **Tip:** Wrappeá solo las partes verdaderamente dinámicas en ` <Suspense> `. Cada boundary de Suspense se convierte en un agujero dinámico en PPR. Si wrappeás toda tu página en un solo Suspense, recreaste SSR con pase es extra. Sé quirúrgico, el shell debería ser la mayoría de la página.\n2. **Tip:** Utilizar ` unstable_noStore() ` en tus funciones de data fetching para marcarlas explícitamente como dinámicas. PPR detecta acceso a datos dinámicos durante el prerender, si un componente llama a ` cookies() `, ` headers() `, o ` searchParams `, se convierte en un agujero dinámico. Pero el marcado explícito previene bugs sutiles.\n3. **Tip:** Probar tus páginas PPR con ` next build ` y verificar el output. Next.js muestra qué páginas son completamente estáticas (ícono círculo), cuáles son PPR (círculo medio lleno), y cuáles son completamente dinámicas (ícono lambda). Si una página que esperabas sea PPR aparece como completamente dinámica, uno de tus componentes top-level está accediendo a datos dinámicos fuera de un boundary de Suspense.\n4. **Tip:** Para páginas de producto de e-commerce, colocar el precio y estado de stock en un boundary de Suspense pero dejar la descripción del producto, imágenes, y reviews en el shell estático. Los precios cambian cada hora pero las descripciones cambian cada mes, matchear tu estrategia de rendering con tus requerimientos de frescura de datos es todo el punto de PPR.\n\n- nextjs\n- ppr\n- partial-prerendering\n- react\n- suspense\n- performance\n- ssr\n- isr\n- generación-estática\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"Next.js 15 PPR: Partial Prerendering, Static Shells, y el Fin del Debate ISR vs SSR","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#title"},{"level":2,"text":"El Problema que PPR Resuelve","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#el-problema-que-ppr-resuelve"},{"level":2,"text":"Cómo Funciona PPR","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#cmo-funciona-ppr"},{"level":2,"text":"Habilitando PPR","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#habilitando-ppr"},{"level":2,"text":"¿Cómo Sabe Next.js Qué es Dinámico?","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#cmo-sabe-nextjs-qu-es-dinmico"},{"level":2,"text":"Mediciones Reales de TTFB","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#mediciones-reales-de-ttfb"},{"level":2,"text":"Migración desde ISR","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#migracin-desde-isr"},{"level":3,"text":"Antes (ISR)","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#antes-isr"},{"level":3,"text":"Después (PPR)","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#despus-ppr"},{"level":2,"text":"Errores Comunes","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#errores-comunes"},{"level":2,"text":"Cuándo NO Usar PPR","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#cundo-no-usar-ppr"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#questions"},{"level":3,"text":"¿Cuál es la diferencia entre PPR e ISR?","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#question-1"},{"level":3,"text":"¿PPR funciona con el Pages Router?","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#question-2"},{"level":3,"text":"¿Cuánto mejora el TTFB con PPR?","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#article-tips"}],"sections":[{"heading":"Next.js 15 PPR: Partial Prerendering, Static Shells, y el Fin del Debate ISR vs SSR","level":1,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering","content":"Frontend"},{"heading":"Next.js 15 PPR: Partial Prerendering, Static Shells, y el Fin del Debate ISR vs SSR","level":1,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#title","content":"2025-12-175 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/next-js-15-ppr-partial-prerendering>)"},{"heading":"El Problema que PPR Resuelve","level":2,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#el-problema-que-ppr-resuelve","content":"Considérese una página típica de producto de e-commerce:\n\n```\n┌──────────────────────────────────────┐\n│ Header (con avatar de usuario) │ ← Dinámico (necesita auth)\n├──────────────────────────────────────┤\n│ Galería de Imágenes del Producto │ ← Estático (cambia mensualmente)\n│ Título y Descripción del Producto │ ← Estático\n│ Reviews del Producto (234 reviews) │ ← Estático (cacheado)\n├──────────────────────────────────────┤\n│ Precio: $49.99 │ ← Dinámico (cambia cada hora)\n│ Stock: quedan 3 │ ← Dinámico (tiempo real)\n│ [Agregar al Carrito] │ ← Dinámico (necesita sesión)\n├──────────────────────────────────────┤\n│ Productos Relacionados │ ← Estático\n│ Footer │ ← Estático\n└──────────────────────────────────────┘\n```\n\nPPR introduce una opción nueva: **servir las partes estáticas instantáneamente desde el CDN, y streamear las partes dinámicas desde el origin en la misma respuesta.**"},{"heading":"Cómo Funciona PPR","level":2,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#cmo-funciona-ppr","content":"PPR en [Next.js](<https://nextjs.org/>) usa boundaries de React Suspense para definir la separación entre contenido estático y dinámico:\n\n```jsx\n// app/products/[slug]/page.jsx\nimport { Suspense } from 'react';\n\nexport default async function ProductPage({ params }) {\n const product = await getProduct(params.slug); // datos estáticos\n\n return (\n <div className=\"product-page\">\n {/* Shell estático, prerenderizado en build time */}\n <ProductDetails product={product} />\n <ProductReviews productId={product.id} />\n\n {/* Agujeros dinámicos, streameados en request time */}\n <Suspense fallback={<PriceSkeleton />}>\n <PricingWidget productId={product.id} />\n </Suspense>\n\n <Suspense fallback={<CartSkeleton />}>\n <CartButton productId={product.id} />\n </Suspense>\n </div>\n );\n}\n```\n\nCuando un usuario pide esta página:\n\n```\nTimeline:\n\n0ms: CDN recibe request\n5ms: CDN sirve shell estático (HTML prerenderizado)\n Usuario ve: imágenes, descripción, reviews, skeletons de carga\n5ms: CDN simultáneamente reenvía request al origin para agujeros dinámicos\n200ms: Origin envía contenido dinámico (precio, stock, botón de carrito)\n Usuario ve: página completa con precio y stock en tiempo real\n```\n\nEl TTFB es 5ms (el shell estático desde CDN), no 200ms (la respuesta SSR completa)."},{"heading":"Habilitando PPR","level":2,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#habilitando-ppr","content":"```javascript\n// next.config.js\n/** @type {import('next').NextConfig} */\nconst nextConfig = {\n experimental: {\n ppr: true,\n },\n};\n\nmodule.exports = nextConfig;\n```\n\nO por ruta:\n\n```javascript\n// app/products/[slug]/page.jsx\nexport const experimental_ppr = true;\n```"},{"heading":"¿Cómo Sabe Next.js Qué es Dinámico?","level":2,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#cmo-sabe-nextjs-qu-es-dinmico","content":"Detecta uso de APIs dinámicas:\n\n```javascript\n// Estos hacen un componente dinámico:\ncookies() // Lee cookies del request\nheaders() // Lee headers del request\nsearchParams // Prop de page desde URL search params\nunstable_noStore() // Opt-out explícito de caching\n\n// Estos siguen siendo estáticos (cacheados en build):\nfetch('https://api.example.com/product/123') // Cacheado por default\n```"},{"heading":"Mediciones Reales de TTFB","level":2,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#mediciones-reales-de-ttfb","content":"Estas mediciones son de una página de listado de productos con 20 productos, cada uno con precio y stock dinámicos:\n\n```\nEstrategia de Rendering TTFB (p50) TTFB (p99) Carga Total\n──────────────────────────────────────────────────────────────────\nSSR (sin cache) 380ms 1200ms 920ms\nISR (revalidación 60s) 45ms 120ms 520ms\nISR + client fetch 45ms 120ms 780ms*\nPPR 42ms 95ms 580ms\n──────────────────────────────────────────────────────────────────\n* ISR + client fetch tiene layout shift cuando cargan los precios\n```\n\nLa ganancia real es la experiencia de usuario. Con ISR + client fetch, la página carga, el usuario ve precios placeholder, después 300ms más tarde los precios reales aparecen con layout shift. Con PPR, el skeleton aparece en el shell estático y se reemplaza seamlessly por el contenido streameado, sin layout shift, sin flash de contenido incorrecto."},{"heading":"Migración desde ISR","level":2,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#migracin-desde-isr","content":""},{"heading":"Antes (ISR)","level":3,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#antes-isr","content":"```javascript\nexport const revalidate = 60;\n\nexport default async function ProductPage({ params }) {\n const product = await getProduct(params.slug);\n const price = await getPrice(product.id); // Stale por hasta 60s\n const stock = await getStock(product.id); // Stale por hasta 60s\n\n return (\n <div>\n <ProductDetails product={product} />\n <div className=\"price\">{price}</div>\n <div className=\"stock\">{stock} restantes</div>\n <AddToCartButton productId={product.id} />\n </div>\n );\n}\n```"},{"heading":"Después (PPR)","level":3,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#despus-ppr","content":"```javascript\nexport const experimental_ppr = true;\n\nexport default async function ProductPage({ params }) {\n const product = await getProduct(params.slug);\n\n return (\n <div>\n <ProductDetails product={product} />\n\n <Suspense fallback={<div className=\"price-skeleton\" />}>\n <DynamicPricing productId={product.id} />\n </Suspense>\n\n <Suspense fallback={<div className=\"stock-skeleton\" />}>\n <DynamicStock productId={product.id} />\n </Suspense>\n\n <Suspense fallback={<div className=\"cart-skeleton\" />}>\n <AddToCartButton productId={product.id} />\n </Suspense>\n </div>\n );\n}\n\nimport { unstable_noStore } from 'next/cache';\n\nasync function DynamicPricing({ productId }) {\n unstable_noStore();\n const price = await getPrice(productId);\n return <div className=\"price\">{formatPrice(price)}</div>;\n}\n\nasync function DynamicStock({ productId }) {\n unstable_noStore();\n const stock = await getStock(productId);\n return (\n <div className={`stock ${stock < 5 ? 'low-stock' : ''}`}>\n {stock > 0 ? `${stock} restantes` : 'Sin stock'}\n </div>\n );\n}\n```"},{"heading":"Errores Comunes","level":2,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#errores-comunes","content":"**Datos dinámicos en layouts se propagan.** Si tu root layout lee ` cookies() ` para preferencia de tema, todo el sitio se vuelve dinámico. Mové la lectura de cookies a un boundary de Suspense dentro del layout.\n\n**El diseño de skeletons importa.** El shell estático es lo que los usuarios ven los primeros 100-300ms. Si tus skeletons no matchean las dimensiones del layout final, teners Cumulative Layout Shift.\n\n```css\n.price-skeleton {\n height: 32px;\n width: 120px;\n background: var(--skeleton-bg);\n border-radius: 4px;\n animation: pulse 1.5s ease-in-out infinite;\n}\n```\n\n**No todo necesita ser dinámico.** Un error común es wrappear demasiadas cosas en Suspense. La mayoría de los datos cambian infrecuentemente y deberían quedarse en el shell estático con revalidación estilo ISR."},{"heading":"Cuándo NO Usar PPR","level":2,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#cundo-no-usar-ppr","content":"- **Páginas completamente estáticas** (blog, docs): Utilizar SSG regular.\n- **Páginas completamente dinámicas** (dashboards, paneles admin): Utilizar SSR regular.\n- **Páginas con contenido altamente personalizado**: Si el 80%+ de la página es user-specific, el shell estático es demasiado fino para proveer mejora significativa de TTFB.\n\nPPR brilla cuando teners páginas mayormente estáticas con algunos widgets dinámicos, e-commerce, sitios de noticias, páginas de marketing con stats en tiempo real, catálogos de productos con precios en vivo. Eso cubre un porcentaje sorprendentemente grande de aplicaciones web en producción.\n\nEl debate ISR vs SSR terminó. PPR no es un compromiso entre los dos, es un modelo de rendering genuinamente nuevo que te da el TTFB de generación estática con la frescura de rendering server-side. La clave es pensar la página como un shell estático con agujeros dinámicos en vez de un render monolítico. Una vez que ese modelo mental hace click, la implementación fluye naturalmente."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#questions","content":""},{"heading":"¿Cuál es la diferencia entre PPR e ISR?","level":3,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#question-1","content":"ISR (Incremental Static Regeneration) sirve una página completamente cacheada y revalida en background después de un timeout. La página entera es stale o fresh. PPR sirve un shell estático inmediatamente (desde el CDN edge) y streamea contenido dinámico en la misma respuesta. Diferentes partes de la página pueden tener diferentes garantías de frescura."},{"heading":"¿PPR funciona con el Pages Router?","level":3,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#question-2","content":"No. PPR requiere el App Router porque depende de React Server Components y boundaries de Suspense para definir el split estático/dinámico. Si se está en el Pages Router, ISR con getStaticProps sigue siendo tu mejor opción para páginas híbridas estáticas/dinámicas."},{"heading":"¿Cuánto mejora el TTFB con PPR?","level":3,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#question-3","content":"En las mediciones, PPR mejora el TTFB un 60-80% comparado con SSR completo porque el shell estático se sirve desde el CDN edge sin tocar el servidor de origen. La mejora en tiempo total de carga es menor (20-40%) porque el contenido dinámico todavía necesita computarse y streamearse. La mejora percibida es aún mayor porque los usuarios ven el shell estático inmediatamente."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#article-tips","content":"1. **Tip:** Wrappeá solo las partes verdaderamente dinámicas en ` <Suspense> `. Cada boundary de Suspense se convierte en un agujero dinámico en PPR. Si wrappeás toda tu página en un solo Suspense, recreaste SSR con pase es extra. Sé quirúrgico, el shell debería ser la mayoría de la página.\n2. **Tip:** Utilizar ` unstable_noStore() ` en tus funciones de data fetching para marcarlas explícitamente como dinámicas. PPR detecta acceso a datos dinámicos durante el prerender, si un componente llama a ` cookies() `, ` headers() `, o ` searchParams `, se convierte en un agujero dinámico. Pero el marcado explícito previene bugs sutiles.\n3. **Tip:** Probar tus páginas PPR con ` next build ` y verificar el output. Next.js muestra qué páginas son completamente estáticas (ícono círculo), cuáles son PPR (círculo medio lleno), y cuáles son completamente dinámicas (ícono lambda). Si una página que esperabas sea PPR aparece como completamente dinámica, uno de tus componentes top-level está accediendo a datos dinámicos fuera de un boundary de Suspense.\n4. **Tip:** Para páginas de producto de e-commerce, colocar el precio y estado de stock en un boundary de Suspense pero dejar la descripción del producto, imágenes, y reviews en el shell estático. Los precios cambian cada hora pero las descripciones cambian cada mes, matchear tu estrategia de rendering con tus requerimientos de frescura de datos es todo el punto de PPR.\n\n- nextjs\n- ppr\n- partial-prerendering\n- react\n- suspense\n- performance\n- ssr\n- isr\n- generación-estática\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/next-js-15-ppr-partial-prerendering"},{"text":"Next.js","url":"https://nextjs.org/"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering"},"headline":"Next.js 15 PPR: Partial Prerendering, Static Shells, y el Fin del Debate ISR vs SSR","description":"Cómo el Partial Prerendering de Next.js 15 combina rendering estático y dinámico en un solo request. Static shell + dynamic holes vía Suspense, mediciones reales de TTFB, y guía práctica de migración desde ISR.","datePublished":"2025-12-17","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Frontend","keywords":"nextjs, ppr, partial-prerendering, react, suspense, performance, ssr, isr, generación-estática","wordCount":936,"timeRequired":"PT5M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/next-js-15-ppr-partial-prerendering"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"Next.js 15 PPR: Partial Prerendering, Static Shells, y el Fin del Debate ISR vs SSR","item":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Cuál es la diferencia entre PPR e ISR?","acceptedAnswer":{"@type":"Answer","text":"ISR (Incremental Static Regeneration) sirve una página completamente cacheada y revalida en background después de un timeout. La página entera es stale o fresh. PPR sirve un shell estático inmediatamente (desde el CDN edge) y streamea contenido dinámico en la misma respuesta. Diferentes partes de la página pueden tener diferentes garantías de frescura."}},{"@type":"Question","name":"¿PPR funciona con el Pages Router?","acceptedAnswer":{"@type":"Answer","text":"No. PPR requiere el App Router porque depende de React Server Components y boundaries de Suspense para definir el split estático/dinámico. Si se está en el Pages Router, ISR con getStaticProps sigue siendo tu mejor opción para páginas híbridas estáticas/dinámicas."}},{"@type":"Question","name":"¿Cuánto mejora el TTFB con PPR?","acceptedAnswer":{"@type":"Answer","text":"En las mediciones, PPR mejora el TTFB un 60-80% comparado con SSR completo porque el shell estático se sirve desde el CDN edge sin tocar el servidor de origen. La mejora en tiempo total de carga es menor (20-40%) porque el contenido dinámico todavía necesita computarse y streamearse. La mejora percibida es aún mayor porque los usuarios ven el shell estático inmediatamente."}}]}]}],"contentHash":"1666d26068659e221c95fed78798f9d1068b46df1760ac60813092acfc3bb1ee","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/next-js-15-ppr-partial-prerendering","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":59501,"contentBytes":11152}}
