wandres.dev
MODOS DE RENDER · SSR, streaming, CSR, SSG

SSG y prerender

Generar HTML en tiempo de build en lugar de por petición: SolidStart delega el prerender en Nitro, que recorre las rutas que le indicas y las congela como HTML estático más el JSON de sus datos, listos para servir desde un CDN sin cómputo. Esta lección cubre la opción server.prerender con su lista de rutas y crawlLinks, cómo prerenderizar rutas dinámicas con parámetros conocidos, y —lo más potente— cómo mezclar en una sola app rutas estáticas y dinámicas con las route rules de Nitro: prerender para lo inmutable, swr e isr para regenerar en segundo plano, ssr para lo que cambia por petición. Incluye la trampa capital: una página prerenderizada se construye una vez y no puede ver cookies ni sesión.

⏱ 17 min

Hay contenido que no cambia entre una petición y la siguiente: una landing, un artículo publicado, una página de precios. Renderizarlo en cada visita es pagar cómputo por un resultado idéntico. El Static Site Generation invierte el momento: renderiza esas rutas una sola vez, en el build, y congela el HTML para servirlo tal cual desde un CDN. SolidStart no reinventa esto: delega en Nitro, su servidor subyacente, que sabe recorrer tus rutas y prerenderizarlas. Y lo verdaderamente valioso no es el todo-estático, sino la mezcla: rutas estáticas y dinámicas conviviendo en una misma app.

🎯 Al terminar esta lección sabrás
  • Prerenderizar rutas en build con server.prerender y descubrir enlaces con crawlLinks.
  • Congelar rutas dinámicas enumerando sus parámetros conocidos en tiempo de build.
  • Componer apps híbridas con las route rules de Nitro: prerender, swr, isr, ssr.
  • Reconocer la trampa capital: una página estática no puede ver datos por petición.

Prerender en build con Nitro

Le indicas a Nitro qué rutas congelar en app.config.ts. En el build, además de empaquetar el cliente, arranca la app, visita cada ruta listada y escribe su HTML resuelto en disco.

// app.config.ts
import { defineConfig } from "@solidjs/start/config";

export default defineConfig({
  server: {
    prerender: {
      routes: ["/", "/precios", "/blog"],
      crawlLinks: true, // sigue los enlaces para descubrir mas rutas
    },
  },
});

Cada ruta prerenderizada produce dos artefactos: el HTML terminado y el JSON de los datos que sus recursos resolvieron. Ese emparejamiento es el que hace que la hidratación no vuelva a pedir nada: el cliente hidrata el HTML estático y lee los datos del JSON que vino con él. crawlLinks: true ahorra mantener la lista a mano: Nitro parte de las rutas semilla, lee los enlaces del HTML generado y encola los que encuentra, cerrando así el sitio por conexidad.

El descubrimiento por rastreo tiene un límite que conviene conocer: solo encuentra lo que está enlazado. Una ruta a la que no llega ningún enlace desde las semillas —una página huérfana, accesible solo por URL directa— pasará inadvertida y no se congelará. Para esos casos la lista explícita sigue siendo la red de seguridad: semillas más rastreo para lo que forma parte del grafo de enlaces, enumeración para lo que vive fuera de él.

Conviene también vigilar el resultado del rastreo en el otro sentido: si crawlLinks descubre rutas que no querías congelar —una zona de administración enlazada por descuido, una vista con datos ya obsoletos— acabarán como estáticos rancios en producción. Revisar la lista de rutas prerenderizadas que el build reporta es una comprobación barata que evita sorpresas, y una buena costumbre antes de cada despliegue.

El resultado es servible sin servidor de aplicación. Cada visita se resuelve leyendo un fichero del CDN: TTFB mínimo, coste por petición nulo, y una superficie de ataque reducida a servir estáticos. Para contenido que no cambia, es difícil batir esta economía.

Hay un detalle que suele sorprender: durante el prerender, tus server functions se ejecutan de verdad, pero en el entorno del build, no en respuesta a una petición. Si un query lee de una base de datos, esa lectura ocurre mientras compilas, y su resultado queda horneado en el JSON que acompaña al HTML. Por eso el prerender exige que los datos estén disponibles en build —la base poblada, las APIs accesibles desde el proceso de compilación— y por eso una ruta cuyos datos solo existen en producción no puede congelarse tal cual sin repensar de dónde salen.

Rutas dinámicas y datos en build

Una ruta con parámetro —/blog/[slug]— no es una sola página sino una familia. Nitro no puede adivinar los slug; o los descubre por crawlLinks siguiendo enlaces desde un índice, o se los enumeras explícitamente para que congele cada variante.

export default defineConfig({
  server: {
    prerender: {
      // Enumera las variantes conocidas de la ruta dinamica.
      routes: ["/blog/introduccion", "/blog/reactividad", "/blog/streaming"],
      crawlLinks: true,
    },
  },
});

Aquí aflora la trampa capital del prerender, y conviene grabarla. Los datos de una página estática se resuelven en el build, no en la visita. Una server function que lea la cookie de sesión, el usuario actual o la hora exacta no tiene sentido en una ruta prerenderizada: en el build no hay petición, no hay cookie, no hay usuario. Lo que congelas es el resultado de ejecutar esa ruta sin contexto de petición. Por eso solo es candidato a SSG el contenido que es igual para todos y estable en el tiempo.

Esa restricción tiene una lectura positiva: te obliga a separar con claridad los datos públicos y estables de los personales y volátiles. Cuando una página mezcla ambos —un artículo estático con un saludo personalizado en la cabecera—, la solución no es renunciar al prerender, sino congelar la parte pública y traer la personal aparte, hidratándola en el cliente o pidiéndola tras la carga. El prerender no prohíbe la personalización; la empuja a su capa correcta, y esa disciplina suele producir arquitecturas más limpias que las que mezclan todo en un render por petición.

Este es, dicho sea de paso, el mismo principio que gobierna el nivel entero: cada trozo de tu página tiene un momento y un lugar naturales donde renderizarse, y el diseño consiste en llevar cada trozo al suyo, en vez de forzar toda la página a un único modo por comodidad. El prerender es simplemente el extremo del «momento» —lo más temprano posible— para el contenido que puede permitírselo.

⚠️
Una página estática no ve la petición

Si prerenderizas una ruta que muestra «Hola, Ana» leyendo la sesión, congelarás el estado que hubiera en el build —vacío— y todos verán lo mismo. El prerender no es SSR adelantado: es SSR sin petición. Cualquier dato personalizado, dependiente de cookies, cabeceras o del reloj, descalifica la ruta como estática. Ese contenido pertenece a SSR o se hidrata aparte en el cliente tras cargar.

flowchart LR
A[build] --> B[Nitro recorre las rutas prerender]
B --> C[HTML estatico mas JSON de datos por ruta]
C --> D[CDN sirve sin computo]
E[ruta dinamica no listada] --> F[servidor la renderiza por peticion]
style C fill:#a6e3a1,color:#11111b
style F fill:#89b4fa,color:#11111b

Una consecuencia práctica: las rutas dinámicas que no enumeras ni se descubren por enlace no dejan de existir —simplemente no se congelan—. Según tu configuración, Nitro las sirve por SSR bajo demanda o devuelve un fallback. Y esto es justo lo que abre la puerta al patrón más útil de todos: prerenderizar el subconjunto conocido y valioso —los cien artículos ya publicados— y dejar que el resto se resuelva dinámicamente. La app no es «estática» ni «dinámica»; es ambas a la vez, decidida ruta por ruta.

Componer estático y dinámico con route rules

La verdadera potencia no es prerenderizar todo, sino mezclar modos en una sola app. Nitro lo permite con las route rules: un mapa de patrones de ruta a políticas de entrega. Una misma base de código sirve así landings congeladas, fichas que se regeneran solas y paneles renderizados por petición.

export default defineConfig({
  server: {
    routeRules: {
      "/": { prerender: true },              // landing: estatica en build
      "/blog/**": { prerender: true },       // articulos: estaticos
      "/producto/**": { swr: 3600 },         // cacheado, revalida en segundo plano
      "/reporte/**": { isr: true },          // se genera al primer acceso y persiste
      "/app/**": { ssr: true },              // panel dinamico por peticion
    },
  },
});

Las políticas cubren el espectro entre lo totalmente estático y lo totalmente dinámico. prerender: true congela en build. swr: 3600 —stale-while-revalidate— sirve la versión cacheada al instante y dispara una regeneración en segundo plano cuando pasa el intervalo, de modo que nadie espera por la actualización. isr —incremental static regeneration, en plataformas que lo soportan— pospone el render al primer acceso y luego persiste el resultado como estático. Y ssr: true deja la ruta plenamente dinámica, resuelta por petición. Eliges por ruta según cuán fresco deba ser el dato y cuánto cómputo estés dispuesto a pagar.

🧊

prerender

Congela en build. Latencia minima y coste nulo por visita. Solo para lo que no cambia por usuario ni en el tiempo.

♻️

swr / isr

Sirve un estatico y revalida en segundo plano. El punto medio para contenido publico que envejece despacio.

🔥

ssr

Render por peticion, con el contexto delante. Para lo dinamico, lo personalizado o lo que exige frescura al segundo.

El orden en que listé las políticas no es casual: van de más estático a más dinámico, y esa es la dirección en la que conviene pensarlas. Empieza preguntando si la ruta puede ser prerender; si no, si tolera swr; si tampoco, si le basta isr; y solo al final, si de verdad exige ssr por petición. Bajar por esa escalera peldaño a peldaño te deja siempre en el más barato que la ruta permite, en lugar de saltar al dinámico por costumbre.

💡
swr e isr: el punto medio entre estático y dinámico

Muchas rutas no son ni inmutables ni personalizadas: un catálogo, una portada de noticias, una ficha de producto con stock que cambia cada tanto. Congelarlas del todo las deja obsoletas; renderizarlas por petición desperdicia cómputo en datos que cambian cada hora, no cada segundo. swr e isr son el término medio: la mayoría de visitas leen un estático rapidísimo, y la frescura se restaura en segundo plano sin bloquear a nadie. Es la opción por defecto sensata para contenido público que evoluciona despacio.

El coste del build y cómo mitigarlo

El prerender no es gratis en el otro extremo del tiempo: lo que ahorras por visita lo pagas en el build. Un sitio con decenas de miles de rutas estáticas puede tardar mucho en compilar, porque cada una se renderiza una vez durante la construcción. Ese coste es aceptable para cientos o unos pocos miles de páginas, pero escala mal cuando el catálogo crece sin techo y cada despliegue arrastra una compilación cada vez más larga.

Ahí es donde isr cambia las reglas. En lugar de congelar todas las rutas en el build, pospone el render de cada una a su primer acceso y luego persiste el resultado como estático para las visitas siguientes. Así el build permanece rápido —no renderiza lo que nadie ha pedido aún— y el catálogo puede ser arbitrariamente grande sin inflar el tiempo de compilación. Es prerender perezoso: la primera visita paga un SSR, todas las demás cobran un estático.

Ese matiz —quién paga la primera visita— es la única diferencia real entre prerender e isr, y decide entre ellos. Si la ruta es crítica y no toleras que el primer visitante espere un render, congélala en el build y que nadie pague. Si el catálogo es enorme y la mayoría de sus rutas se visitan rara vez, deja que isr las genere bajo demanda: pagarás un SSR ocasional por página poco vista a cambio de un build que no se dispara. Es el canje clásico entre coste amortizado por adelantado y coste diferido al primer uso.

La lección de fondo es que estático y dinámico no son una frontera sino un continuo, y swr e isr son sus estaciones intermedias. Cuanto más adelantas el render hacia el build, más barata es la visita y más caro y rígido el build; cuanto más lo retrasas hacia la petición, más flexible y fresco el resultado y más cómputo pagas por visita. Elegir dónde parar en ese continuo es toda la decisión, y se toma ruta por ruta.

Una advertencia sobre swr e isr que suele pillar desprevenido: sirven contenido deliberadamente rancio durante la ventana de revalidación. Para un catálogo o un blog eso es inofensivo; para un precio, un saldo o un stock, servir un valor viejo aunque sea unos minutos puede ser inaceptable. La frescura tolerada es un requisito de negocio, no un detalle técnico, y es quien fija el intervalo de revalidación —o quien descarta la revalidación en favor de un SSR por petición—.

El prerender no es un modo: es adelantar el reloj del render

Conviene ver SSG y SSR no como dos técnicas rivales sino como el mismo acto de render ejecutado en dos instantes distintos del tiempo. Renderizar es siempre lo mismo: correr tus componentes, resolver sus datos, producir HTML. La única variable que SSG cambia es cuándo ocurre eso —en el build, una vez, para todos— frente al SSR, que lo hace en cada petición, con el contexto de esa petición delante. De esa sola diferencia temporal se derivan todas las demás. El prerender es imbatible en coste y latencia precisamente porque paga el render una vez y lo amortiza en infinitas visitas; y es incapaz de personalizar precisamente porque, al adelantar el reloj al build, se ejecuta antes de que exista petición alguna que personalizar. No hay magia ni pérdida: hay un intercambio limpio entre frescura y coste, gobernado por el momento del render. Entenderlo así disuelve las falsas dicotomías. La pregunta deja de ser «¿estático o dinámico?» y pasa a ser «¿qué tan atrás en el tiempo puedo mover el render de esta ruta sin perder algo que importe?». Para una landing, hasta el build: nada de lo que muestra depende de quién mira. Para un catálogo que cambia cada hora, hasta el último swr: casi todo el tiempo sirve algo pre-renderizado y solo revalida cuando el dato envejeció. Para un panel de usuario, ni un instante antes de la petición: todo lo que muestra nace de quién pide. Las route rules de Nitro no son más que un dial para fijar, ruta por ruta, ese momento del render en la línea temporal que va del build a la petición —y dominar los modos de renderizado es, en el fondo, dominar ese dial—.

⚔️ Construye una app híbrida por rutas
  1. Prerenderiza / y /precios con server.prerender y confirma en la carpeta de salida del build que existe el HTML estático de cada una junto a su JSON de datos.
  2. Añade crawlLinks: true, enlaza varios artículos desde un índice y verifica que Nitro descubre y congela las rutas de /blog/** sin listarlas a mano.
  3. Prerenderiza una ruta que lea la sesión y observa que muestra el estado del build para todos; explica por qué el prerender no ve la petición.
  4. Aplica route rules: prerender a la landing, swr a una ficha de producto y ssr a un panel; comprueba cómo cada patrón se sirve distinto.
  5. Razona, para cinco rutas de un proyecto tuyo, cuánto puedes «adelantar el reloj» del render —build, swr, isr o petición— y justifica cada elección por su necesidad de frescura.