wandres.dev
SSR, SSG E HÍBRIDO · output y prerender

Los modos de render en Astro 7

Renderizar es convertir un componente en HTML; la única pregunta es cuándo ocurre. Astro 7 no ofrece tres modos rivales sino un eje —build o petición— con un valor por defecto que fija `output` y un interruptor por página. Estático por defecto, servidor bajo demanda, qué produce cada build y por qué `output: 'hybrid'` dejó de existir.

⏱ 15 min

Renderizar, en Astro, es una sola cosa: convertir un componente en el HTML que verá el navegador. Lo único que varía —y lo que da nombre a este nivel entero— es cuándo sucede esa conversión. Puede ocurrir una vez, durante el build, y quedar congelada en un fichero que se sirve idéntico a todo el mundo; o puede ocurrir viva, en cada petición, calculada para quien pregunta en el instante en que pregunta. Astro 7 no te entrega tres modos enfrentados entre los que escoger de una vez por todas: te entrega un solo eje —build o petición— con un valor por defecto para el proyecto entero y un interruptor que accionas página por página.

🎯 Al terminar esta lección sabrás
  • Distinguir el renderizado en el build (estático) del renderizado bajo demanda (servidor).
  • Fijar el valor por defecto del proyecto con output y conectar un adaptador cuando toque.
  • Leer lo que cada build deja en disco para saber qué has construido de verdad.
  • Entender por qué output: 'hybrid' desapareció y ver el modo como un eje continuo.

Un solo eje: build o petición

Conviene desmontar cuanto antes la idea de que SSG, SSR e híbrido son tres tecnologías distintas. Renderizar un componente .astro es siempre el mismo cálculo: tomas datos, evalúas el frontmatter, produces marcado. La distinción que importa no está en cómo se renderiza, sino en cuándo se ejecuta ese cálculo y, por tanto, quién carga con su coste y qué información tiene delante.

Hay exactamente dos momentos posibles. Uno es el build: el cálculo corre una sola vez cuando publicas, su resultado se guarda como un fichero HTML plano y ese fichero se sirve tal cual a cada visita. Es lo que se llama estático o SSG. El otro es la petición: el cálculo corre en un servidor cada vez que alguien pide la página, con los datos frescos de ese momento y, si hace falta, particularizado para ese visitante. Es lo que se llama bajo demanda o SSR.

Todo lo demás de este nivel —el rendimiento, el coste, la frescura de los datos, qué puedes leer de la petición— se deduce de esa única elección temporal. Por eso la palabra modo engaña: no eliges una maquinaria, eliges un momento. Y como el momento se decide para cada página por separado, lo que de verdad tienes no son tres cajas, sino un dial que cada ruta ajusta a su naturaleza.

Vale la pena insistir en que el código que escribes es casi el mismo en ambos momentos. El frontmatter de una página —sus import, su await, su lógica— corre igual se ejecute en el build o en la petición; lo que cambia no es lo que tecleas, sino cuándo se evalúa y con qué contexto disponible. Esa casi identidad es una buena noticia práctica: mover una página de un momento al otro rara vez exige reescribirla, solo cambiar una línea y revisar qué datos de petición asume. Aprender un momento es, en gran medida, aprender el otro.

Conviene grabar el contraste esencial, porque de él cuelga todo lo demás del nivel:

  • En el build: corre una vez, produce un fichero, sirve idéntico a todos, coste fijo, datos congelados hasta el próximo despliegue.
  • En la petición: corre por visita, produce una respuesta viva, particularizable por visitante, coste por uso, datos frescos del instante.

output: el valor por defecto, no una jaula

El fichero de configuración fija con output cuál de los dos momentos es el valor por defecto de todo el proyecto. Con output: 'static' —lo que trae un proyecto nuevo— cada página se hornea en el build salvo que digas lo contrario. Con output: 'server', cada página se renderiza bajo demanda salvo que la excluyas explícitamente.

---
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'static',            // por defecto: hornear en el build
  // output: 'server',         // por defecto: renderizar por peticion
  adapter: node({ mode: 'standalone' }),
});

La palabra clave es por defecto. output no encierra tus páginas en un régimen único: solo declara qué momento se asume cuando una página no dice nada. Cada valor implica una postura sobre la mayoría de tus rutas:

  • output: 'static' asume que casi todo es contenido horneable y trata lo dinámico como excepción puntual.
  • output: 'server' asume que casi todo depende de la petición y trata lo estático como la excepción que se congela.

En cuanto una sola ruta necesita renderizarse bajo demanda —en un proyecto por lo demás estático— Astro exige un adaptador, el paquete que traduce tu sitio al runtime que lo ejecutará: Node, Vercel, Cloudflare, Netlify. Sin adaptador no hay servidor vivo que atienda esas páginas, y el build fallará avisándote. El adaptador es la frontera real entre los dos mundos: un sitio puramente estático no necesita ninguno, porque produce ficheros que cualquier CDN sirve; en el momento en que una ruta pasa a bajo demanda, tu despliegue deja de ser una carpeta de ficheros y pasa a incluir una función de servidor. Esa es la consecuencia arquitectónica de cruzar el eje, y conviene tenerla presente antes de accionar el interruptor.

Un ejemplo concreto de excepción: en un proyecto estático, una sola página que muestra un perfil por sesión se saca del horno con una línea, y el resto del sitio sigue horneándose sin enterarse.

---
// src/pages/perfil.astro  (proyecto con output: 'static')
export const prerender = false;   // solo ESTA ruta se sirve viva
const usuario = await usuarioActual(Astro.cookies);
---
<h1>Perfil de {usuario.nombre}</h1>

Qué produce cada build

La diferencia entre los dos momentos se palpa en lo que el build deja en disco. Un proyecto enteramente estático produce una carpeta de ficheros —un HTML por ruta, más los assets— y nada más: no hay proceso que arrancar, solo contenido que subir a cualquier alojamiento.

# output: 'static' sin rutas dinamicas -> solo ficheros
dist/
  index.html
  blog/primer-post/index.html
  _astro/          # css y js de las islas

En cuanto una ruta pasa a bajo demanda, el build emite además un punto de entrada de servidor —una función que el adaptador sabe ejecutar en su runtime— junto a los ficheros de las rutas que sí se hornearon.

# una ruta prerender=false, u output: 'server' -> aparece un servidor
dist/
  client/          # lo estatico y los assets
  server/          # la funcion que atiende lo dinamico

Mirar el dist/ es la forma más honesta de saber qué has construido de verdad. Si solo hay HTML, tienes un sitio que cualquiera aloja gratis en un bucket. Si aparece una carpeta server/, has adquirido una dependencia operativa —algo que ejecutar, vigilar y pagar— y conviene que sea porque alguna ruta lo necesitaba, no por descuido.

⚠️
El servidor de desarrollo engaña: usa astro preview

Cuidado con fiarte de astro dev para juzgar el modo de render. El servidor de desarrollo ejecuta todo bajo demanda por comodidad —para que veas los cambios al instante— aunque tus páginas vayan a hornearse en producción. Eso significa que una página que lee la petición parece funcionar en desarrollo y luego falla al desplegarla si olvidaste marcarla como dinámica. Para ver el comportamiento real, ejecuta astro build seguido de astro preview: ahí lo estático es estático de verdad, y los fallos de modo salen a la luz antes de llegar a producción.

El fin del modo híbrido

Quien venga de versiones antiguas recordará un tercer valor, output: 'hybrid', para sitios mayormente estáticos con algún trozo dinámico. Se retiró, y su desaparición es una lección de diseño. Resultó que static ya era híbrido por naturaleza —estático por defecto, con rutas puntuales marcadas como dinámicas— así que la tercera palabra no añadía ningún poder: solo duplicaba, con otro nombre, lo que la bandera por página ya permitía.

Que una opción desaparezca y el sistema no pierda expresividad es raro y revelador. No se esfumó ninguna capacidad: static con prerender hace todo lo que hybrid hacía, y además con un concepto menos que aprender. Cuando un rediseño elimina vocabulario sin quitar poder, suele ser señal de que se ha encontrado el modelo correcto —el que describe el problema con las menos piezas posibles.

📝
Dos valores y una bandera cubren todo el espectro

Con output reducido a static y server, y con export const prerender disponible en cada página, tienes acceso a cualquier combinación imaginable sin memorizar un tercer modo. Un blog estático con un buscador dinámico, o una aplicación de servidor con su portada congelada, se expresan igual de bien: eliges el valor por defecto que minimice las excepciones y marcas a mano las rutas que se salen de la norma. Menos vocabulario, misma expresividad.

Así que el mapa completo es sencillo. La combinación por defecto de static es “casi todo horneado, alguna ruta viva”; la de server es “casi todo vivo, alguna ruta congelada”. Ambas recorren el mismo eje desde extremos opuestos, y ambas admiten mezcla. Elegir entre las dos no es elegir capacidades —las capacidades son idénticas— sino elegir cuál de los dos regímenes describe mejor la mayoría de tus páginas, para escribir la mínima cantidad de excepciones.

📝
La elección de output es reversible y barata

Ninguna decisión de este eje es un compromiso de por vida. Cambiar output, o poner una bandera prerender en una página, es una edición de una línea que no toca la lógica del componente. Puedes empezar estático, descubrir que una sección necesita servidor y añadir el adaptador sin reescribir nada; o devolver una ruta a estático el día que deje de necesitar frescura. Diseña sabiendo que el momento de render es un ajuste que se afloja y se aprieta, no una arquitectura tallada en piedra.

📄

output: static

El valor por defecto. Todo se hornea en el build; marca rutas sueltas como dinámicas con prerender false.

🖥️

output: server

Todo se renderiza por peticion; congela rutas concretas con prerender true. Exige un adaptador.

🔀

El eje, no el modo

Build o peticion es un dial por pagina. output solo fija el valor por defecto del proyecto.

🔌

El adaptador

En cuanto una ruta es bajo demanda hace falta un adaptador que la ejecute en Node, Vercel o el edge.

flowchart TD
R[renderizar es componente a HTML] --> Q{cuando se calcula}
Q -->|en el build| S[estatico SSG]
Q -->|en cada peticion| D[bajo demanda SSR]
S --> SF[fichero plano en la CDN]
D --> DA[requiere adaptador de servidor]
style R fill:#89b4fa,color:#11111b
style SF fill:#a6e3a1,color:#11111b
style DA fill:#f9e2af,color:#11111b
💡
Empieza por estático y justifica cada excepción

La regla práctica más sana es tratar output: 'static' como punto de partida y considerar cada ruta bajo demanda una decisión que hay que justificar. No porque lo dinámico sea malo, sino porque lo estático es casi gratis de operar —ficheros en una CDN— y lo dinámico introduce un servidor que hay que ejecutar, escalar y pagar. Partir de lo barato y añadir coste solo donde rinde es una disciplina que este nivel entero desarrolla.

No hay modos de render, solo un momento que eliges por página

La forma más madura de entender el renderizado en Astro es dejar de verlo como un menú de modos y verlo como una única variable temporal: ¿cuándo se calcula esta página? Los nombres heredados —SSG, SSR, híbrido— sugieren tres máquinas distintas, tres mentalidades que aprender por separado, y esa sugerencia es falsa. La máquina es una sola: el mismo compilador, el mismo componente, el mismo cálculo de datos a marcado. Lo único que cambia es el instante en que ese cálculo corre, y con él, quién paga su coste y qué información tiene disponible. Precalcular en el build compra velocidad absoluta y coste de operación casi nulo a cambio de servir a todos lo mismo y quedar tan fresco como la última publicación. Calcular en la petición compra frescura y personalización a cambio de trabajo por visita y de un servidor que mantener. Ni uno ni otro es superior; son respuestas a preguntas distintas sobre la naturaleza de cada página. Que Astro haya destilado esa elección hasta dejar dos valores de output y una bandera por ruta no es una simplificación cosmética: es la señal de que el framework ha entendido el problema hasta su hueso. La complejidad accidental —tres modos, adaptadores implícitos, reglas especiales— se ha evaporado, y queda solo la complejidad esencial, la única que no se puede eliminar porque es la pregunta misma: para esta página concreta, ¿me conviene pagar una vez y servir a todos igual, o pagar en cada visita y servir a cada uno lo suyo? El día que dejas de preguntarte “¿qué modo uso?” y empiezas a preguntarte “¿cuándo quiero que se calcule esto?”, has cruzado de usar Astro a entenderlo.

⚔️ Recorre el eje de lado a lado
  1. Crea un proyecto con output: 'static' y confirma que npm run build produce solo ficheros HTML, sin ninguna carpeta server/.
  2. Añade un adaptador con astro add node y marca una página con export const prerender = false; reconstruye y observa que ahora el dist/ incluye un servidor.
  3. Cambia output a 'server' y marca esa misma página con export const prerender = true; comprueba que el comportamiento observable no cambia, solo el valor por defecto.
  4. Razona, para tres páginas de un sitio real que uses, cuál sería su momento de render natural y cuántas excepciones escribirías bajo cada valor de output.