Web Design and Engineering30 de julio de 20267 min de lectura

Partial Prerendering estable en Next.js 16: cómo estructurar rutas

Next.js 16 hace estable Partial Prerendering con cacheComponents: un shell estático desde el edge de la CDN y las partes dinámicas en streaming.

a very tall building with lots of scaffolding around it

Partial Prerendering es un modelo de renderizado de Next.js que sirve una route como shell estático y transmite en streaming sus partes dinámicas dentro de la misma respuesta HTTP. El shell (layout, navegación, cabeceras, todo lo que es idéntico para cada visitante) sale del edge de la CDN en pocos milisegundos. Las partes personalizadas (una fila del dashboard, el total de un carrito, un feature flag) se generan en request time y rellenan los placeholders que React ya ha dibujado.

Pasó a estable en Next.js 16, publicado en octubre de 2025. El antiguo flag experimental.ppr ya no existe. Ahora se activa mediante un modelo más amplio llamado Cache Components. Si construyes un SaaS sobre el App Router, esto cambia cómo decides qué es estático y qué es dinámico, route por route. Antes la elección era todo-o-nada por página. Ahora es por component.

La versión en 30 segundos

Antes de PPR, una route de Next.js era estática o dinámica. Una llamada a cookies(), un fetch sin cache, y la página entera se volvía dinámica: cada visitante esperaba al servidor, la CDN no cacheaba nada y el time to first byte seguía a la query más lenta. PPR rompe ese compromiso. El shell estático se pre-renderiza en build time y se sirve desde el edge. El trabajo dinámico ocurre solo dentro de los boundary Suspense, y solo esas partes esperan. El lector ve el layout al instante y observa cómo se rellenan los huecos dinámicos.

¿Por qué hicieron falta tres versiones?

PPR llegó como flag experimental en Next.js 14, se pulió en la 15 y pasó a estable en la 16. El retraso no fue marketing. La parte difícil es la corrección: Next.js tiene que demostrar, en build time, qué partes de un árbol de component son de verdad estáticas y cuáles dependen de los datos de la petición, y luego serializar la salida estática más un blob "postponed" que deja al servidor retomar el renderizado de las partes dinámicas en cada petición. Si te equivocas en ese límite, o filtras los datos de un usuario en la cache de otro, o lo vuelves todo dinámico y pierdes el sentido.

Next.js 16 resolvió la ergonomía dándole la vuelta al valor por defecto. Con Cache Components todo es dinámico salvo que elijas lo contrario. Marcas lo que va en cache con la directiva 'use cache' y marcas lo que debe seguir dinámico envolviéndolo en Suspense. El framework deja de adivinar.

¿Cómo funciona de verdad Partial Prerendering?

Tres fases, y conviene tenerlas separadas en la cabeza.

Build time. Next.js renderiza las partes de la route que no dependen de los datos de la petición: el layout, el texto estático y la UI de fallback de cada boundary Suspense. Lo guarda como un shell HTML estático más un blob serializado del renderizado en pausa.

Request time. El shell sale primero, directo desde el edge de la CDN. Como son los mismos bytes para todos, la latencia del edge (unos 20-50ms) es todo el coste. El servidor retoma entonces el renderizado en pausa, produce las secciones dinámicas y las transmite por la misma conexión con chunked transfer encoding. Sin segunda petición.

Hydration. React hidrata cada boundary Suspense en su sitio, a medida que llega su chunk. El usuario lee el shell mientras los huecos aún cargan, y en eso consiste todo el argumento de rendimiento.

Vale la pena leer el mecanismo entero en el artículo de un ingeniero del core de Next.js, enlazado en las fuentes. Para la arquitectura importa la regla que implica: un component es dinámico si, y solo si, lee datos en request time (cookies(), headers(), searchParams o un fetch sin cache) y queda fuera de un boundary cacheado.

Qué ganas, con números

La ganancia concreta es el time to first byte. Cuando la route entera es dinámica, el TTFB espera al origin y a la base de datos. Con PPR el shell es estático, así que el TTFB equivale a la distancia entre el usuario y el nodo edge más cercano, no a la velocidad de la query más lenta. Un informe de producción midió una route pasando de 850ms a menos de 100ms de TTFB tras volver estático el shell (fuente más abajo). Los números que veas serán distintos. El mecanismo no: estático-desde-el-edge gana a dinámico-desde-el-origin en el first byte siempre, y el first byte es lo que miden tanto los Core Web Vitals como los usuarios impacientes.

Cómo estructurar las rutas de un SaaS en torno a PPR

Aquí es donde el modelo se gana el sitio. Una página SaaS rara vez es toda estática o toda dinámica. Es un marco estático alrededor de unas pocas celdas personalizadas. Estructura la route en consecuencia.

Empuja el límite dinámico lo más hondo posible. No envuelvas todo el cuerpo de la página en un solo Suspense. Envuelve el subárbol más pequeño que de verdad lee los datos de la petición. El shell de un dashboard, la sidebar y la rejilla de cards vacías pueden ser estáticos; solo los números dentro de cada card son dinámicos. Cuanto más árbol dejes en el shell estático, más página aparece al instante.

Cachea lo compartido, personaliza el resto. Las páginas de marketing, la documentación, los precios y la superficie de usuario sin sesión son shells estáticos con casi nada dinámico. Las rutas de la app lo invierten: un marco estático alrededor de celdas dinámicas. Un solo framework sirve ambas sin cambiar de modo de renderizado.

Dale a cada hueco un fallback de verdad. El fallback de Suspense no es un impuesto de spinner. Forma parte del shell estático, así que sale al instante y define el layout. Usa skeletons que respeten el tamaño final del contenido, o el shell se recolocará cuando lleguen los datos y suspenderás el Cumulative Layout Shift.

Vigila qué pones en el layout. Una sola lectura de cookies() en un layout raíz, fuera de un boundary, devuelve la route entera al renderizado dinámico. Es la forma más común de quedarte fuera de PPR sin darte cuenta. Lee auth y locale dentro de un component acotado, no arriba del todo.

Mantén encendida la observabilidad del streaming. PPR solo compensa si puedes ver llegar las partes dinámicas. Mide el TTFB aparte de la carga completa, y vigila la cola: un hueco dinámico lento ya no bloquea el shell, pero sigue bloqueando los datos por los que el usuario ha venido.

Cuándo no usarlo

PPR no es complejidad gratis. Sáltatelo cuando la route entera es de verdad dinámica, un feed personalizado donde nada se comparte, porque no hay ningún shell estático que extraer y añades configuración sin ganancia. Sáltatelo para contenido del todo estático (un artículo de blog, una página de documentación) que ya era rápido sin él. Y trata el opt-in como una migración, no como un interruptor: activar cacheComponents vuelve todo dinámico por defecto, así que una app existente necesita que cada superficie cacheada se marque con 'use cache' antes de comportarse como antes. En un codebase App Router grande eso es trabajo real, hecho route por route.

En corto: PPR convierte "estático o dinámico" de veredicto por página en decisión por component. Para un SaaS, donde casi cada página es un marco estático alrededor de un poco de dato vivo, es el modelo de renderizado que el App Router debería haber tenido desde el principio. En Next.js 16 llega por fin como camino por defecto, no como experimento.

Foto de Paul Becker en Unsplash

Preguntas frecuentes

¿Es estable Partial Prerendering en Next.js 16?

Sí. PPR pasó a estable en Next.js 16 (octubre de 2025). El flag experimental.ppr y la config de segmento experimental_ppr se han eliminado. Ahora lo activas mediante Cache Components, poniendo cacheComponents: true en next.config.ts, que también desbloquea la directiva use cache. Las versiones anteriores (14 y 15) lo mantenían tras un flag experimental, así que el código escrito para ellas necesita el cambio de config.

¿Qué diferencia hay entre Partial Prerendering y streaming SSR?

El streaming SSR renderiza toda la página en el servidor en cada petición y la transmite sobre la marcha, así que el first byte sigue esperando al servidor. PPR pre-renderiza un shell estático en build time y lo sirve desde el edge de la CDN, y luego transmite solo los huecos dinámicos desde el origin. Así PPR te da layout instantáneo para cada visitante más streaming para las partes que lo necesitan. Usa Suspense igual que el streaming, pero el shell alrededor de esos boundary ya está construido.

¿Tengo que reescribir la app para activar Partial Prerendering?

Reescribir no, migrar sí. Activar cacheComponents da la vuelta al valor por defecto: cada route se vuelve dinámica salvo que marques los datos cacheados con use cache y acotes las lecturas dinámicas en Suspense. En una app pequeña es una tarde. En un codebase App Router grande, hazlo route por route y vigila las lecturas en request time (cookies, headers) dentro de los layouts, porque una de ellas deja la route entera fuera del shell estático.

¿Necesita Vercel Partial Prerendering?

No. PPR es una función de Next.js, no solo de Vercel. El shell estático es HTML pre-renderizado normal y las partes dinámicas viajan sobre una respuesta HTTP en chunks normal, así que cualquier host que ejecute Next.js 16 puede servirlo. El beneficio pleno en el TTFB llega cuando una CDN va delante y sirve el shell estático desde el edge, algo que montas en Cloudflare, una CDN self-hosted o tu propio reverse proxy. Sin cache en el edge el shell sale igual primero, solo que desde tu origin.

Artículos relacionados

Studio

Empieza un proyecto.

Un partner único para todo el proyecto. Producción más rápida, tecnología moderna, costes reducidos.