Datos: request-time frente a build-time
El momento de render se paga en tres monedas que no puedes maximizar a la vez: frescura de los datos, latencia de la primera respuesta y coste de operación. Pedir un dato en el build o en la petición reparte esas monedas de forma opuesta. Anatomía del TTFB, la caché como palanca y la economía de pagar una vez frente a pagar por visita.
La elección del momento de render no es abstracta: se cobra en tres monedas muy concretas —la frescura de los datos, la latencia de la primera respuesta y el coste de operar el sitio— y su ley de hierro es que no puedes maximizar las tres a la vez. Pedir un dato en el build y pedirlo en la petición reparten esas monedas de forma casi opuesta: lo que uno gana en velocidad y baratura lo pierde en frescura, y al revés. Este es el nivel donde el eje deja de ser una idea de diseño y se vuelve una hoja de cálculo.
- Contrastar obtener datos en el build con obtenerlos en cada petición.
- Analizar de dónde sale el TTFB en cada caso.
- Situar la caché como la palanca que media entre frescura y coste.
- Estimar el coste de operación de lo estático frente a lo bajo demanda.
Dos momentos para pedir un dato
El mismo await fetch produce consecuencias distintas según cuándo corra. En una página horneada, la petición de datos ocurre una vez, durante el build; su resultado se cocina dentro del HTML y ese HTML se sirve inmutable hasta la próxima publicación. El dato es tan viejo como tu último despliegue, pero servirlo no cuesta ni una llamada.
---
export const prerender = true; // el fetch corre UNA vez, en el build
const productos = await fetch('https://api.tienda/productos').then((r) => r.json());
---
<ul>{productos.map((p) => <li>{p.nombre}</li>)}</ul>
En una página bajo demanda, ese mismo fetch corre en cada visita. El dato está siempre fresco —refleja el estado del origen en el instante de pedirlo— pero cada visitante paga la espera de esa llamada, y tu servidor la ejecuta una y otra vez.
---
export const prerender = false; // el fetch corre en CADA peticion
const productos = await fetch('https://api.tienda/productos').then((r) => r.json());
---
El código es idéntico salvo por una línea. Lo que cambia por completo es el reparto de las tres monedas: el build compra latencia mínima y coste nulo a cambio de frescura; la petición compra frescura a cambio de latencia y coste. No hay un fetch mejor que el otro, solo dos momentos con precios distintos.
Y entre esos dos extremos hay un continuo, no un abismo. Estas son algunas paradas intermedias, ordenadas de más fresco a más barato:
- Bajo demanda sin caché: cada visita recomputa. Frescura al segundo, coste por visita.
- Bajo demanda con caché corta: se guarda el resultado unos segundos o minutos. Casi fresco, coste amortizado.
- Rebuild programado: reconstruyes el sitio cada hora o cada día. Estático, pero con datos periódicamente renovados.
- Estático puro: se hornea una vez y vive hasta el próximo despliegue. Máxima velocidad y baratura, mínima frescura.
Todas esas tácticas mueven el mismo dial —cada cuánto se paga el cálculo— sin saltar de golpe de “una vez para siempre” a “en cada visita”. Pensar en frescura como un espectro, y no como un interruptor, es lo que abre las estrategias intermedias.
Caché y TTFB: de dónde sale la latencia
El TTFB —el tiempo hasta el primer byte— tiene una anatomía distinta en cada modo, y entenderla explica por qué lo estático se siente instantáneo. Para una página horneada, el visitante habla con la CDN: el único coste es la distancia física hasta el nodo del edge más cercano, que ya tiene el fichero listo. No hay cómputo, no hay origen, no hay base de datos en el camino. Milisegundos.
Para una página bajo demanda sin caché, la cadena es más larga: la petición viaja de la CDN al origen, el origen ejecuta tu función, tu función quizá espera a una API o a una base de datos, y solo entonces vuelve el primer byte. Desglosado, el TTFB de una página dinámica suele repartirse en cuatro sumandos:
- El salto de red de la CDN al origen que ejecuta la función.
- El arranque de la función si estaba dormida —el temido arranque en frío—.
- Tu cómputo: el render del componente y la lógica del frontmatter.
- La espera a la fuente: la API o la base de datos que consultas.
Solo el tercero está enteramente en tus manos; los otros tres explican por qué a veces optimizar tu código apenas mueve el TTFB, y por qué una fuente lenta arruina la latencia por muy afinado que esté tu render.
Entre los dos extremos vive la palanca que lo cambia todo: la caché. Una respuesta bajo demanda puede llevar una cabecera Cache-Control que autoriza a la CDN a guardarla y servirla a los siguientes visitantes sin volver a molestar a tu servidor. La primera petición paga el cómputo completo; las siguientes, durante la ventana que fijes, se sirven desde el edge a velocidad de estático. Con revalidación, además, la CDN puede seguir sirviendo la copia guardada mientras refresca la nueva por detrás. Así, Cache-Control te deja elegir un punto intermedio en el eje: datos casi frescos a un coste casi nulo, regulando cada cuánto se recomputa de verdad.
// Un endpoint bajo demanda cacheado en el edge
export const prerender = false;
export const GET = async () => {
const datos = await consultarFuente();
return new Response(JSON.stringify(datos), {
headers: { 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300' },
});
};
Merece la pena distinguir las tres piezas de esa cabecera, porque cada una gobierna un actor distinto. max-age le habla al navegador del visitante; s-maxage le habla a la CDN compartida, y suele ser el que de verdad quieres afinar; stale-while-revalidate autoriza a servir la copia vencida un rato mientras se recalcula por detrás, de modo que ningún visitante espera al recómputo. Y hay un detalle crítico: la caché compartida solo es lícita si la respuesta es igual para todos. En cuanto personalizas por cookie o cabecera, ese s-maxage público se vuelve peligroso, porque repartiría a unos los datos de otros.
El punto dulce moderno tiene nombre propio: revalidación. En lugar de hornear en cada build o computar en cada visita, sirves una versión guardada y la refrescas cuando caduca o cuando un evento avisa de que el dato cambió. La primera petición tras la caducidad recomputa por detrás mientras el resto sigue recibiendo la copia rápida. Así una ficha de producto se ve instantánea como un estático y, sin embargo, refleja el stock con minutos de retraso en vez de días. Muchos adaptadores de Astro exponen esta capacidad a través de las cabeceras de caché de su plataforma, y es la respuesta habitual cuando ni hornear ni computar en vivo encajan del todo.
El coste: pagar una vez o pagar por visita
La tercera moneda es la que menos se mira y más factura. Lo estático concentra su coste en el build: minutos de cómputo que pagas una vez al publicar, y a partir de ahí, ancho de banda barato de una CDN. Su propiedad más valiosa es que el coste no crece con el tráfico: servir el mismo fichero a mil o a un millón de personas cuesta prácticamente lo mismo, y una avalancha de visitas no despierta ningún servidor.
Lo bajo demanda invierte esa economía. No hay coste de build que hablar, pero cada petición invoca una función de servidor que se cobra: por invocación, por milisegundo de cómputo, por la llamada a la fuente de datos. El coste escala con el tráfico, linealmente. Diez veces más visitas es diez veces más cómputo facturado. Para lo que de verdad necesita ser dinámico, ese gasto está justificado; para lo que podría haberse horneado, es dinero quemado en recalcular eternamente la misma respuesta.
Un ejemplo aterriza la asimetría. Imagina una página vista un millón de veces al mes. Horneada, su coste es un puñado de segundos de build más el ancho de banda de la CDN, iguales tanto si la ven mil personas como un millón. Bajo demanda sin caché, ese millón de vistas son un millón de invocaciones de función y un millón de consultas a la fuente, una factura que crece en línea recta con el éxito. Si el contenido era el mismo para todos, has pagado un millón de veces por calcular lo que podías haber calculado una sola vez, y el tráfico —que debería ser una buena noticia— se convierte en una cuenta creciente.
La lección económica se resume en dos frases:
- Lo que es igual para todos: págalo una vez en el build; repetir el pago por visita es puro desperdicio.
- Lo que cambia por visita o por usuario: no hay atajo, cada respuesta cuesta, y la caché solo ayuda si varias visitas comparten el mismo resultado.
Cada extremo tiene además su coste oculto, y conviene nombrarlos para no llevarse sorpresas:
- Lo estático paga su precio en el tiempo de build: un sitio de cientos de miles de páginas puede tardar mucho en construirse, y refrescar un solo dato obliga a reconstruir. Aquí es donde el compilador en Rust y Rolldown de Astro 7 marcan la diferencia.
- Lo dinámico paga en arranques en frío y en la latencia de la fuente: una función dormida tarda en despertar, y una base de datos lenta se traga tu TTFB por mucho que tu código sea rápido.
La decisión no arranca en la tecnología sino en una pregunta sobre el dato: ¿cuánto puede estar desactualizado sin que a nadie le importe? El precio de un producto tolera minutos u horas de retraso; el saldo de una cuenta, ninguno; un artículo de blog, días. Ese presupuesto de frescura es el dato de entrada, y de él se deduce el momento de render: frescura holgada permite hornear o cachear con una ventana amplia; frescura nula obliga a calcular en la petición sin caché compartida. Elegir al revés —fijar primero el modo y esperar que la frescura salga bien— es como comprar el envase antes de saber qué guardas: a veces cabe, a veces derramas datos viejos sobre el usuario.
Build-time
El dato se pide una vez y se hornea. Latencia minima, coste fijo, frescura hasta el ultimo build.
Request-time
El dato se pide en cada visita. Siempre fresco, con latencia y coste que escalan con el trafico.
Caché
Cache-Control guarda respuestas dinamicas en el edge: la segunda visita va a velocidad de estatico.
La factura
Lo estatico no crece con el trafico; lo dinamico se cobra por invocacion y por milisegundo.
flowchart TD
V[visita a la pagina] --> M{modo de render}
M -->|estatico| CDN[fichero listo en la CDN]
M -->|bajo demanda| DYN[el servidor ejecuta y espera a la fuente]
CDN --> FAST[primer byte en milisegundos]
DYN --> CACHE{hay Cache Control}
CACHE -->|ya cacheado| FAST
CACHE -->|sin cache| SLOW[primer byte tras computo y latencia]
style V fill:#89b4fa,color:#11111b
style FAST fill:#a6e3a1,color:#11111b
style SLOW fill:#f38ba8,color:#11111bLa intuición sobre latencia y coste engaña; los números, no. Antes de convertir una página a bajo demanda por “frescura” o de cachearla por “velocidad”, mide: el TTFB real en las herramientas del navegador, el tiempo de build, la factura de invocaciones. Muchas veces descubrirás que la fuente de datos —no el modo de render— es la que domina el TTFB, o que un rebuild nocturno bastaba donde ibas a pagar cómputo en cada visita. El momento de render es una palanca poderosa, pero solo la mueves bien cuando ves la magnitud que estás moviendo.
Bajo toda la discusión de SSG, SSR y caché late una sola idea, y es una de las más universales de la computación: el momento de render es la elección de cuándo pagas el coste de un cálculo. Precalcular en el build es pagar por adelantado, una vez, y amortizar ese pago entre todas las visitas futuras; el resultado se guarda y se reparte, de modo que cuantas más veces se sirve, más barata sale cada entrega. Calcular en la petición es pagar en el punto de uso, cada vez, sin amortización posible: cada visita repite el gasto entero. La caché es, exactamente, un mecanismo de amortización injertado a posteriori sobre lo dinámico: paga la primera vez y reparte ese pago entre las siguientes, empujando lo bajo demanda hacia la economía de lo estático durante una ventana de tiempo. Es la misma dualidad que recorre la informática entera —compilar contra interpretar, memoizar contra recomputar, indexar contra escanear, precargar contra pedir al vuelo— y en todas ellas aparece el mismo triángulo imposible: frescura, latencia y coste, de los que puedes tener dos a la vez pero nunca los tres en su máximo. Lo estático te da latencia mínima y coste mínimo sacrificando frescura. Lo dinámico te da frescura máxima sacrificando latencia y coste. La caché con revalidación te acerca a tener casi los tres, pero solo aflojando la frescura a “casi al día” en vez de “al segundo”. No existe la configuración que gane las tres monedas, porque el triángulo no es una limitación de Astro sino una propiedad del tiempo: un dato solo puede estar simultáneamente precalculado y actualizado si el mundo se detiene entre el cálculo y la lectura, y el mundo no se detiene. Por eso el ingeniero maduro no busca el modo “correcto”, sino que decide, para cada dato, cuánta frescura exige de verdad y coloca el cálculo en el momento que la sirve al menor coste. Renderizar bien no es elegir estático o dinámico: es saber cuánto vale la frescura de cada cosa y pagar por ella exactamente lo justo, ni un milisegundo ni un céntimo de más.
- Pon el mismo
fetchen una página conprerender = truey otra conprerender = false; cambia el dato en el origen y observa cuál se actualiza sin rebuild. - Mide el TTFB de ambas con las herramientas del navegador y explica la diferencia a partir de la cadena que recorre cada petición.
- Añade una cabecera
Cache-Controlcons-maxagea la página dinámica y comprueba cómo cae el TTFB de la segunda visita. - Estima, para un pico de tráfico de cien mil visitas, el coste relativo de servir esa página horneada frente a servirla bajo demanda sin caché.