El modelo de renderizado del App Router de Next.js, explicado bien
Estático, dinámico, en streaming y revalidado no son cuatro ajustes — son un modelo con unos pocos interruptores. Saber cuál has accionado es la mayor parte del trabajo de rendimiento en Next.js.
Casi todos los problemas de rendimiento de Next.js que nos llaman a diagnosticar se reducen a la misma frase: nadie sabía que esta ruta se había vuelto dinámica.
No es que el renderizado dinámico esté mal. Es que el paso de estático a dinámico es implícito. Ocurre porque alguien leyó una cookie cuatro componentes más abajo, y nada en la pull request lo decía.
Este artículo es el modelo mental que enseñamos a todos los equipos con los que trabajamos.
No hay cuatro modos de renderizado
Hay un modelo — renderizar el árbol de componentes en el servidor — y la única pregunta real es cuándo ocurre ese renderizado y cuánto se reutiliza.
| Cuándo ocurre el renderizado | Cómo suele llamarse |
|---|---|
| En tiempo de build, reutilizado siempre | Estático |
| En tiempo de build, reemplazado ante una señal | Incremental Static Regeneration |
| En cada petición | Dinámico |
| Build para la cáscara, petición para los huecos | Partial Prerendering |
El streaming es ortogonal a todo esto. El streaming trata del orden de entrega, no de cuándo se hace el trabajo.
Qué vuelve dinámica una ruta de verdad
Una ruta es estática hasta que algo en ella necesita la petición. Leer cualquiera de estos saca la ruta del renderizado estático:
import { cookies, headers } from 'next/headers';
import { connection } from 'next/server';
await cookies(); // needs the request
await headers(); // needs the request
await connection(); // explicitly asks for the requestsearchParams en las props de una página hace lo mismo. También fetch con
cache: 'no-store', y también cualquier llamada a unstable_noStore que haya
quedado de una refactorización anterior.
Lo importante es que esto se contagia hacia arriba, no hacia abajo. Un
Server Component en lo profundo del árbol que lee cookies() vuelve dinámica
la ruta entera, porque la ruta no puede prerenderizarse si alguna de sus
partes necesita una petición que todavía no existe.
Por eso la pregunta de auditoría nunca es «¿esta página es estática?». Es: «¿qué es lo más profundo de este árbol que toca la petición, y necesita hacerlo?».
El accidente habitual
// app/products/[slug]/page.tsx
export default async function Page({ params }) {
const { slug } = await params;
const product = await getProduct(slug);
return (
<>
<ProductDetail product={product} />
<RecentlyViewed /> {/* reads cookies() — the whole page is now dynamic */}
</>
);
}El detalle de producto podría haber sido estático durante un año. Una franja personalizada en la esquina hizo que cada petición volviera a renderizarlo todo.
La solución no es borrar la franja. Es empujarla detrás de un límite de Suspense para que el resto de la página pueda prerenderizarse:
<Suspense fallback={<RecentlyViewedSkeleton />}>
<RecentlyViewed />
</Suspense>Con Partial Prerendering activado, la cáscara estática se sirve desde el edge de inmediato y solo el hueco se calcula por petición. Sin él, al menos recibes la cáscara en streaming primero, en lugar de que espere la página entera.
La revalidación es una señal, no un temporizador
La mayoría de los equipos recurre a un revalidate por tiempo porque es la
primera opción de la documentación:
export const revalidate = 3600;Eso es una conjetura sobre cada cuánto cambia tu contenido. La revalidación por etiquetas es un hecho sobre cuándo cambió:
// Reading side
const posts = await fetch(`${API}/posts`, {
next: { tags: ['posts'] },
});
// Writing side — your CMS webhook route
import { revalidateTag } from 'next/cache';
export async function POST(request: Request) {
await verifyWebhookSignature(request);
revalidateTag('posts');
return Response.json({ revalidated: true });
}La diferencia en la práctica: con un temporizador de una hora, tu redacción publica una corrección y luego recarga durante cincuenta minutos. Con una etiqueta, la corrección está en vivo en segundos, y la página se sirve desde caché indefinidamente el resto del tiempo. Más fresco y más barato.
La memoización de peticiones no es la caché de datos
Se confunden constantemente, y resuelven problemas distintos.
La memoización de peticiones deduplica llamadas fetch idénticas dentro
de un mismo pase de renderizado. Existe para que puedas llamar a getUser()
en el layout y otra vez en la página sin pagar dos viajes de ida y vuelta. Es
por renderizado y se evapora cuando el renderizado termina.
La caché de datos persiste entre peticiones y despliegues. Es sobre ella
que operan revalidate y revalidateTag.
Si envuelves una fuente de datos que no es fetch — un cliente de base de
datos, por ejemplo — no obtienes memoización gratis. Usa cache de React:
import { cache } from 'react';
import 'server-only';
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});Sin ese envoltorio, un layout y tres componentes que necesitan el usuario actual lanzan cuatro consultas idénticas por renderizado. Hemos encontrado exactamente eso en producción más veces de las que nos gustaría reconocer.
Cómo auditar una ruta en cinco minutos
- Ejecuta
next buildy lee la tabla de rutas.○es estática,●es SSG con parámetros,ƒes dinámica. Todo lo que seaƒsin esperarlo es tu lista. - Para cada sorpresa, busca en el árbol
cookies,headers,searchParams,no-storeyconnection. - Pregúntate si esos datos hacen falta para el primer pintado o solo para un fragmento personalizado. Los fragmentos van detrás de Suspense.
- Comprueba que toda función de datos que no use
fetchesté envuelta encache. - Sustituye el
revalidatepor tiempo por etiquetas allí donde exista un camino de escritura.
Esa lista es la primera hora de la mayoría de los proyectos de rendimiento que hacemos, y suele ser donde se esconde la mayor ganancia individual.
Qué te da esto
Una ruta que se renderiza estáticamente sirve su HTML desde un nodo edge de CDN en milisegundos de un solo dígito. La misma ruta renderizada dinámicamente ejecuta tu árbol de componentes, tu capa de datos y tu base de datos en cada petición. La diferencia no es un porcentaje. Es un orden de magnitud, y aparece en el LCP, en el coste de infraestructura y en lo que le pasa a tu sitio cuando un enlace se vuelve viral un martes por la tarde.
