Ir al contenido
Web

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.

31 de julio de 2026 · 7 min de lectura · Actualizado el 12 de octubre de 2026

A solid grey page shell with three holes; one is filled white, and chunks of white stream from the right into the other two

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.

Preguntas frecuentes

Servicios relacionados

Artículos relacionados

Studio

Empieza un proyecto.

Escribimos sobre lo que construimos. Cuéntanos qué quieres construir tú.