Renderizado Estático vs Dinámico
Entiende cuándo Next.js renderiza rutas en tiempo de compilación (estático) versus tiempo de solicitud (dinámico), y cómo controlarlo.
Receta
Tarjeta de referencia rápida -- lista para copiar y pegar.
// Estático de forma predeterminada -- sin funciones dinámicas, sin búsquedas sin caché
export default async function AboutPage() {
return <h1>About Us</h1>; // Construido en tiempo de despliegue
}
// Se vuelve dinámico automáticamente al usar funciones dinámicas
import { cookies } from "next/headers";
export default async function DashboardPage() {
const cookieStore = await cookies(); // Esto opta al renderizado dinámico
return <h1>Dashboard</h1>;
}
// Fuerza estático o dinámico con configuración de segmento
export const dynamic = "force-dynamic"; // Siempre renderiza en tiempo de solicitud
export const dynamic = "force-static"; // Siempre renderiza en tiempo de compilación
// Pre-renderiza rutas dinámicas en tiempo de compilación
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map((p) => ({ slug: p.slug }));
}Cuándo usarlo: Necesitas entender por qué una ruta es estática o dinámica, o quieres controlar explícitamente el comportamiento de renderizado.
Ejemplo Funcional
// app/blog/[slug]/page.tsx -- Generación estática con generateStaticParams
import { notFound } from "next/navigation";
type Post = { slug: string; title: string; content: string; date: string };
async function getPost(slug: string): Promise<Post | null> {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { tags: [`post-${slug}`] },
});
if (!res.ok) return null;
return res.json();
}
// Pre-renderiza estas rutas en tiempo de compilación
export async function generateStaticParams() {
const res = await fetch("https://api.example.com/posts");
const posts: Post[] = await res.json();
return posts.map((post) => ({
slug: post.slug,
}));
}
// Genera metadatos para cada publicación
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) return { title: "Not Found" };
return { title: post.title };
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return (
<article className="prose max-w-2xl mx-auto p-6">
<h1>{post.title}</h1>
<time className="text-gray-500">{post.date}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}// app/dashboard/page.tsx -- Renderizado dinámico (usa cookies)
import { cookies } from "next/headers";
import { db } from "@/lib/db";
import { verifySession } from "@/lib/auth";
export default async function DashboardPage() {
const cookieStore = await cookies();
const token = cookieStore.get("session")?.value;
const session = await verifySession(token);
if (!session) {
return <p>Please sign in</p>;
}
const stats = await db.userStats.findUnique({
where: { userId: session.userId },
});
return (
<main className="p-6">
<h1 className="text-2xl font-bold">Welcome, {session.user.name}</h1>
<div className="grid grid-cols-3 gap-4 mt-6">
<StatCard label="Orders" value={stats?.orders ?? 0} />
<StatCard label="Revenue" value={`$${stats?.revenue ?? 0}`} />
<StatCard label="Customers" value={stats?.customers ?? 0} />
</div>
</main>
);
}
function StatCard({ label, value }: { label: string; value: string | number }) {
return (
<div className="border rounded p-4">
<p className="text-sm text-gray-500">{label}</p>
<p className="text-2xl font-bold">{value}</p>
</div>
);
}Lo que esto demuestra:
- Una página de publicación de blog que se genera estáticamente en tiempo de compilación usando
generateStaticParams - Una página de panel que se renderiza dinámicamente porque llama a
cookies() - Generación de metadatos para páginas estáticas
- El resultado de la búsqueda se almacena en caché y se revalida por etiqueta
Inmersión Profunda
Cómo Funciona
- De forma predeterminada, Next.js intenta renderizar estáticamente cada ruta en tiempo de compilación. Una ruta permanece estática siempre que:
- No llame a funciones dinámicas (
cookies(),headers(),searchParams) - No use
cache: "no-store"en ninguna búsqueda - No exporte
dynamic = "force-dynamic"
- No llame a funciones dinámicas (
- Funciones dinámicas que optan una ruta al renderizado dinámico:
cookies(),headers(),searchParams(la propiedad page),useSearchParams()(sin Suspense), yconnection(). generateStaticParamsle dice a Next.js qué segmentos de ruta dinámica pre-renderizar en tiempo de compilación. Las rutas no devueltas por esta función se renderizam bajo demanda (y se almacenan en caché) o devuelven 404, dependiendo dedynamicParams.- Regeneración Estática Incremental (ISR) usa
revalidatepara reconstruir páginas estáticas en segundo plano después de un intervalo de tiempo. La primera solicitud después del intervalo obtiene la página obsoleta; las solicitudes posteriores obtienen la versión nueva. - Las páginas estáticas se sirven desde el edge de CDN con cero computación de servidor. Las páginas dinámicas requieren un servidor (runtime Node.js o Edge) para cada solicitud.
Variaciones
Controlando el comportamiento para rutas no en generateStaticParams:
// Permite renderizado de rutas no pre-generadas (comportamiento predeterminado)
export const dynamicParams = true;
// Devuelve 404 para rutas no pre-generadas
export const dynamicParams = false;Revalidación a nivel de segmento:
// Revalida todas las páginas bajo este layout cada 60 segundos
export const revalidate = 60;
// Nunca revalida (completamente estático hasta el próximo despliegue)
export const revalidate = false;Mezclando estático y dinámico en la misma página con Suspense:
import { Suspense } from "react";
import { cookies } from "next/headers";
// El shell estático se renderiza en tiempo de compilación
export default function Page() {
return (
<main>
<h1>Product Page</h1> {/* Estático */}
<StaticProductInfo /> {/* Estático */}
<Suspense fallback={<p>Loading cart...</p>}>
<DynamicCartPreview /> {/* Dinámico -- llama a cookies() */}
</Suspense>
</main>
);
}
async function DynamicCartPreview() {
const cookieStore = await cookies();
const cartId = cookieStore.get("cart-id")?.value;
// ...
}Notas de TypeScript
// Tipo de retorno de generateStaticParams
export async function generateStaticParams(): Promise<{ slug: string }[]> {
// ...
}
// Para rutas dinámicas anidadas
export async function generateStaticParams(): Promise<{
category: string;
slug: string;
}[]> {
// ...
}
// Tipos de configuración de segmento
export const dynamic: "auto" | "force-dynamic" | "force-static" | "error" = "auto";
export const dynamicParams: boolean = true;
export const revalidate: number | false = false;
export const runtime: "nodejs" | "edge" = "nodejs";Trampas
-
Una única llamada a
cookies()hace que toda la ruta sea dinámica -- Incluso si el 99% de la página es contenido estático, una función dinámica opta toda la ruta al renderizado dinámico. Solución: Usa Partial Prerendering (PPR) o aísla la parte dinámica en un boundary de Suspense. -
generateStaticParamsse ejecuta solo en tiempo de compilación -- El contenido nuevo agregado después de la compilación no se pre-renderiza hasta la próxima compilación o a menos quedynamicParamsseatrue(que renderiza bajo demanda). Solución: ManténdynamicParams: true(el predeterminado) para que las nuevas rutas se rendericen en la primera solicitud y luego se almacenen en caché. -
force-staticcon funciones dinámicas lanza un error -- Si exportasdynamic = "force-static"pero la página llama acookies(), la compilación falla. Solución: Elimina la función dinámica o cambia adynamic = "auto". -
revalidate: 0no significa "revalidar inmediatamente" -- Significa "siempre dinámico." Solución: Usa un número positivo para el comportamiento de ISR. -
Páginas estáticas con búsqueda de datos del lado del cliente -- Una página puede generarse estáticamente pero aún así obtener datos en el cliente con SWR o React Query. El HTML estático sirve como el shell. Solución: Este es un patrón válido, no un error -- solo sé consciente del estado inicial vacío.
Alternativas
| Enfoque | Úsalo Cuando | No lo Uses Cuando |
|---|---|---|
| Generación Estática (predeterminada) | El contenido cambia con poca frecuencia, se puede compilar en tiempo de despliegue | Los datos son específicos del usuario o en tiempo real |
ISR (revalidate: N) | El contenido cambia periódicamente, quieres velocidad de CDN con frescura | Los datos deben estar frescos en cada solicitud |
| Renderizado Dinámico | Los datos son específicos del usuario, usa cookies o headers | El contenido es igual para todos los usuarios |
| Partial Prerendering (PPR) | Quieres un shell estático con agujeros dinámicos | La página completa es completamente estática o completamente dinámica |
| Búsqueda del lado del cliente | Necesitas actualizaciones en tiempo real después de que carga la página | Los datos se pueden obtener completamente en el servidor |
force-static | Quieres garantizar que una ruta nunca sea dinámica | La ruta genuinamente necesita datos en tiempo de solicitud |
Preguntas Frecuentes
¿Cómo decide Next.js si una ruta es estática o dinámica?
Una ruta es estática de forma predeterminada. Se vuelve dinámica si:
- Llama a una función dinámica (
cookies(),headers(),searchParams,connection()) - Usa
cache: "no-store"en una búsqueda - Exporta
dynamic = "force-dynamic"
¿Qué es generateStaticParams y cuándo se ejecuta?
generateStaticParamsle dice a Next.js qué segmentos de ruta dinámica pre-renderizar en tiempo de compilación.- Se ejecuta solo en tiempo de compilación, no en tiempo de solicitud.
- Las rutas no devueltas se renderizan bajo demanda (si
dynamicParamsestrue) o devuelven 404.
¿Cuál es la diferencia entre generación estática e ISR?
- Generación estática: las páginas se construyen una vez en tiempo de despliegue y nunca cambian hasta la próxima compilación.
- ISR (
revalidate: N): las páginas se reconstruyen en segundo plano después de N segundos. La primera solicitud después del intervalo obtiene la página obsoleta; las solicitudes posteriores obtienen la versión nueva.
Trampa: Una única llamada a cookies() hace que mi página completa sea dinámica. ¿Cómo arreglo esto?
- Usa Partial Prerendering (PPR) para mantener el shell estático y hacer streaming de la parte dinámica.
- Alternativamente, aísla el componente dinámico dentro de un boundary de
<Suspense>para que solo esa porción sea dinámica.
Trampa: ¿Qué pasa si exporto force-static pero la página llama a cookies()?
La compilación fallará con un error. No puedes forzar una ruta a ser estática cuando usa funciones dinámicas. Elimina la función dinámica o cambia a dynamic = "auto".
¿Qué significa revalidate: 0?
Significa siempre renderiza dinámicamente (equivalente a force-dynamic). No significa "revalidar inmediatamente." Usa un número positivo (por ejemplo, revalidate: 60) para el comportamiento de ISR.
¿Puede una página generada estáticamente aún obtener datos en el cliente?
Sí. Una página puede generarse estáticamente pero aún así obtener datos en el cliente usando SWR o React Query. El HTML estático sirve como el shell, y los datos del lado del cliente se rellenan después de la hidratación.
¿Cómo controlo qué sucede para rutas no devueltas por generateStaticParams?
// Permite renderizado bajo demanda para rutas desconocidas (predeterminado)
export const dynamicParams = true;
// Devuelve 404 para rutas desconocidas
export const dynamicParams = false;¿Cómo escribo el valor de retorno de generateStaticParams en TypeScript?
export async function generateStaticParams(): Promise<
{ slug: string }[]
> {
const posts = await fetchAllPosts();
return posts.map((p) => ({ slug: p.slug }));
}Para rutas dinámicas anidadas, incluye todos los parámetros de segmento en el tipo de retorno.
¿Cuáles son los tipos válidos de TypeScript para la exportación de configuración de segmento dinámico?
export const dynamic: "auto" | "force-dynamic" | "force-static" | "error" = "auto";
export const dynamicParams: boolean = true;
export const revalidate: number | false = false;
export const runtime: "nodejs" | "edge" = "nodejs";¿Cómo puedo mezclar contenido estático y dinámico en la misma página?
Envuelve la parte dinámica en un boundary de <Suspense>. El shell estático se renderiza en tiempo de compilación, y el componente dinámico (por ejemplo, uno que llama a cookies()) hace streaming en tiempo de solicitud.
¿Desde dónde se sirven las páginas estáticas versus las dinámicas?
- Las páginas estáticas se sirven desde el edge de CDN con cero computación de servidor.
- Las páginas dinámicas requieren un servidor (runtime Node.js o Edge) para cada solicitud.
Relacionado
- Server Components -- Componentes asincronos que permiten renderizado estático
- Partial Prerendering -- Híbrido estático + dinámico
- Revalidación -- ISR y revalidación bajo demanda
- Almacenamiento en Caché -- Cómo las capas de almacenamiento en caché determinan el modo de renderizado