Static vs Dynamic Rendering en Next.js App Router: diferencias y ejemplos
Introducción
En el ecosistema de Next.js, el App Router introdujo un nuevo modelo de renderizado que combina lo mejor del servidor y del cliente. Sin embargo, esta flexibilidad trae consigo una pregunta frecuente: ¿cuándo se renderiza de forma estática y cuándo de forma dinámica? Entender la diferencia no solo afecta al rendimiento, sino también al SEO, al caché y a la experiencia de usuario.
Este artículo está dirigido a desarrolladores que ya conocen los conceptos básicos de React y Next.js y quieren profundizar en cómo el App Router decide entre renderizado estático y dinámico. Al final, sabrás qué dispara cada modo, cómo aprovechar sus ventajas y evitar los errores más comunes.
¿Qué significa "renderizado" en el App Router?
En Next.js 13+ el App Router trata a todos los componentes como Server Components por defecto. Un Server Component se ejecuta en el servidor y su resultado (HTML) se envía al navegador. Dependiendo de qué datos utiliza y de qué APIs del entorno llama, Next.js puede decidir:
- Renderizado estático: el HTML se genera en tiempo de construcción (o durante una revalidación) y se reutiliza para todas las solicitudes.
- Renderizado dinámico: el HTML se genera en cada solicitud (o en un caché corto) porque depende de datos que solo están disponibles en tiempo de request.
Esta decisión se toma automáticamente, pero podés influir en ella mediante el uso de ciertas funciones y datos.
Renderizado estático (Static Rendering)
¿Cómo funciona?
Cuando un Server Component no accede a ninguna fuente de datos que varíe por request (como cookies(), headers(), searchParams, o funciones que dependan del estado de usuario), Next.js lo marca como estático. El resultado se almacena en el caché de generación estática (ISR o SSG) y se sirve igual a todos los usuarios hasta que se invalide.
Cuándo se usa
- Fetch de datos externos en
fetchcon la opción{ cache: 'force-cache' }(valor por defecto). - Uso de funciones puras que no dependen del request (por ejemplo, leer un archivo local, llamar a una API sin headers personalizados).
- Componentes que solo muestran información de producto, blog, documentación, etc., donde el contenido no cambia frecuentemente.
Ventajas
- Máxima performance: el HTML se entrega desde un CDN sin tocar el servidor de aplicación.
- Mejor SEO: los crawlers reciben contenido completamente renderizado.
- Caché compartido: una misma respuesta sirve a miles de usuarios.
Limitaciones
- No podés mostrar datos personalizados (por ejemplo, el nombre del usuario logueado).
- Si necesitás reflejar cambios en tiempo real, tenés que esperar a la revalidación.
Ejemplo práctico
Supongamos un blog donde cada publicación se obtiene de un CMS headless en tiempo de construcción.
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
export const dynamic = 'force-static'; // opcional, deja claro la intención
interface Post {
id: string;
title: string;
content: string;
}
// Función que simula una llamada al CMS (en realidad se ejecuta en build)
async function getPost(slug: string): Promise<Post | null> {
// En un caso real sería await fetch(`https://mi-cms.com/posts/${slug}`)
const posts: Record<string, Post> = {
'primer-post': {
id: '1',
title: 'Mi primer post',
content: 'Bienvenido a mi blog.',
},
};
return posts[slug] ?? null;
}
export default async function PostPage({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}Salida esperada (HTML simplificado)
Para cualquier visita a /blog/primer-post el servidor devolverá exactamente:
<article>
<h1>Mi primer post</h1>
<div>Bienvenido a mi blog.</div>
</article>Este mismo HTML se sirve desde el caché aunque miles de usuarios lo soliciten simultáneamente.
Renderizado dinámico (Dynamic Rendering)
¿Cómo funciona?
Si un Server Component lee alguna dato que varía por request (por ejemplo, cookies, encabezados, searchParams, o llama a una función marcada como unstable_noStore o revalidate: 0), Next.js lo considera dinámico. En ese caso, el HTML se genera en cada solicitud (o se guarda en un caché de corta duración si usas ISR con un redefine bajo).
Cuándo se activa
- Uso de
cookies()oheaders()desdenext/navigation. - Lectura de
searchParamsconuseSearchParams()(en un Server Component) o directamente desdeparamsque provienen de la URL y pueden cambiar. - Llamada a una API externa sin caché (
fetch(..., { cache: 'no-store' })). - Uso de
unstable_noStore()para desactivar cualquier caché. - Dependencia de datos de usuario (por ejemplo,
getServerSessionde NextAuth).
Ventajas
- Personalización: podés mostrar información específica del usuario, como su nombre, carrito o preferencias.
- Datos siempre frescos: ideal para paneles de administración, feeds en tiempo real o cualquier cosa que no deba estar almacenada en caché.
Limitaciones
- Mayor carga en el servidor de aplicación porque cada request genera HTML.
- Posible impacto en el TTFB (Time To First Byte) si la lógica es pesada.
- Menor beneficio de caché CDN; depende de la estrategia de revalidación que elijas.
Ejemplo práctico
Imaginemos un panel de control donde se muestra el nombre del usuario logueado y la hora actual del servidor.
// app/dashboard/page.tsx
import { cookies } from 'next/navigation';
import { unstable_noStore } from 'next/cache';
export const dynamic = 'force-dynamic'; // opcional, deja explícito
async function getUserName(): Promise<string> {
// Simulamos lectura de una cookie que contiene el token JWT
const token = cookies().get('auth-token')?.value;
if (!token) return 'Invitado';
// En un caso real verificaríamos el token y obteneríamos el nombre
return 'Juan Pérez';
}
export default async function Dashboard() {
unstable_noStore(); // forzamos renderizado en cada request
const userName = await getUserName();
const now = new Date().toLocaleTimeString();
return (
<section>
<h2>Panel de control</h2>
<p>Hola, <strong>{userName}</strong>!</p>
<p>Hora del servidor: {now}</p>
</section>
);
}Salida esperada (HTML simplificado)
Cada vez que se solicite /dashboard el servidor producirá algo similar a:
<section>
<h2>Panel de control</h2>
<p>Hola, <strong>Juan Pérez</strong>!</p>
<p>Hora del servidor: 14:23:07</p>
</section>Si el usuario cambia (o la hora avanza), el HTML será diferente en la siguiente solicitud.
Incremental Static Regeneration (ISR): el punto intermedio
Next.js permite que un componente estático se vuelva a generar en segundo plano después de un tiempo determinado. Esta técnica se llama ISR y brinda los beneficios del caché estático con la posibilidad de actualizar el contenido sin rebuild completo.
Cómo funciona ISR en el App Router
- Exportás una función
revalidateen unlayout.tsopage.ts(o usás la opciónfetch(..., { next: { revalidate: 60 } })). - La primera request después del tiempo de expiración dispara una regeneración en segundo plano; la request actual sigue recibiendo la versión cached.
- Las siguientes requests reciben la versión recién generada.
Ejemplo de ISR
// app/products/page.tsx
import { notFound } from 'next/navigation';
// Esta función se ejecutará en build y luego se revalidará cada 60 segundos
export const revalidate = 60;
async function fetchProducts() {
const res = await fetch('https://api.ejemplo.com/products', {
// next: { revalidate: 60 } también funciona aquí
});
if (!res.ok) throw new Error('Error al obtener productos');
return res.json();
}
export default async function ProductsPage() {
const products = await fetchProducts();
return (
<ul>
{products.map(p => (
<li key={p.id}>{p.name} – ${p.price}</li>
))}
</ul>
);
}Qué ocurre en la práctica
- En la primera visita (o después de 60 s sin visitas) Next.js genera el HTML llamando a la API y lo guarda en caché.
- Durante los próximos 60 s, cualquier usuario recibe ese mismo HTML sin tocar la API.
- Al pasar el minuto, la siguiente request activa una nueva llamada a la API en segundo plano; mientras tanto, el usuario ve la versión anterior.
- Una vez que la nueva generación termina, el caché se actualiza y los siguientes visitantes ven la lista actualizada.
Buenas prácticas y errores comunes
Buenas prácticas
- Declará la intención: aunque Next.js infera el modo, usar
export const dynamic = 'force-static'oexport const dynamic = 'force-dynamic'hace el código más legible y evita sorpresas. - Minimizá el uso de datos dinámicos: si solo necesitás un dato de usuario (como el nombre), considerá cargarlo del cliente con
useEffecto una SWR request, manteniendo el Server Component estático. - Aproveché el caché de fetch: usar
fetch(..., { next: { revalidate: 60 } })permite obtener datos relativamente frescos sin volver a renderizar todo el layout. - Separá lo estático de lo dinámico: dividí la UI en componentes; dejá los que dependen de request en archivos separados y mantené el resto estático.
- Monitoreá el caché: revisá los encabezados
AgeyX-Nextjs-Cacheen las respuestas para confirmar si estás recibiendo una versión estática o dinámica.
Errores comunes
- Olvidar que
cookies()yheaders()son solo Server: intentar usarlos en un Client Component provoca error; mové esa lógica al Server Component o pasá los valores como props. - Usar
fetchsin opciones de caché y esperar que sea estático: por defecto,fetchen Next.js usaforce-cache, pero si alguna vez agregaste{ cache: 'no-store' }sin darte cuenta, pasarás a renderizado dinámico y aumentarás la carga del servidor. - Confundir
revalidateconcache: 'no-store':revalidatesigue generando una versión estática que se actualiza periódicamente;no-storeelimina cualquier caché y fuerza renderizado en cada request. - Renderizar datos de usuario en el layout: si el layout lee
cookies(), todo el árbol debajo se vuelve dinámico, incluso partes que podrían ser estáticas (como el footer). Envolvé esas secciones en componentes separados o pasá los datos como props. - No manejar errores en datos estáticos: si la fetch falla en tiempo de build, la construcción fallará. Para ISR, manejá el error y mostrá un fallback o una página de error.
Conclusión
El App Router de Next.js brinda un modelo de renderizado único donde la decisión entre estático y dinámico depende exclusivamente de los datos que tus Server Components consumen. Al comprender qué dispara cada modo, podés:
- Aprovechar el poder del caché CDN para páginas públicas y de alto tráfico.
- Mantener la personalización y la frescura de los datos donde realmente lo necesitás.
- Usar ISR como puente para obtener lo mejor de ambos mundos.
Recordá siempre declarar tu intención con dynamic o revalidate cuando el comportamiento no sea obvio, y separar claramente las responsabilidades entre componentes estáticos y dinámicos. Con estas prácticas, tus aplicaciones Next.js serán más rápidas, más escalables y ofrecerán una mejor experiencia tanto a los usuarios como a los motores de búsqueda.
Próximos pasos: experimentá con un pequeño proyecto que tenga una lista de productos (estática con ISR) y un panel de usuario (dinámico). Observá los encabezados de respuesta y los tiempos de carga para ver la diferencia en la práctica. ¡Éxitos!
Palabras clave: Next.js App Router, static rendering, dynamic rendering, ISR, SSR, SSG, Server Components, caché, rendimiento, SEO.
DUGLAS MORENO
Comentarios (0)
Sé el primero en comentar.