Rendimiento extremo: presupuesto de JS y caché multinivel
Un 100/100 no es suerte sino un presupuesto defendido ruta a ruta. Astro parte de cero JavaScript, así que la perfección es el estado por defecto que hay que evitar arruinar: fijar un presupuesto de JS por ruta, entender qué mide cada Core Web Vital, y diseñar una caché en tres niveles —CDN, server islands y datos— que se refuerzan entre sí.
El presupuesto de JavaScript por ruta
Astro no envía JavaScript salvo que tú lo pidas con una directiva de cliente, y cada isla que hidratas gasta de un presupuesto. Ponerle número a ese presupuesto —y por ruta, no global— es lo que separa un sitio rápido de uno que se degrada solo con el tiempo.
Un reparto razonable: las rutas de contenido puro —blog, docs— viven en cero KB y ahí deben quedarse; una landing de marketing puede permitirse una isla ligera por debajo de los 30 KB; una ruta de aplicación tolera más porque su valor lo justifica. La clave es que el número existe y se vigila: cuando una isla nueva amenaza con romper el presupuesto de una ruta de contenido, esa es la señal de que quizá no debería ser una isla.
---import Contador from '../components/Contador.jsx';import Comentarios from '../components/Comentarios.jsx';---<!-- Cada directiva es una decision de presupuesto --><Contador client:visible /> <!-- se hidrata al entrar en pantalla --><Comentarios client:idle /> <!-- espera a que el navegador este ocioso -->La directiva correcta es media dosis de rendimiento. client:load hidrata de inmediato y compite con el arranque de la página; client:visible y client:idle difieren ese coste hasta que hace falta o el hilo principal está libre. Y recuerda la aritmética de runtimes: dos islas de React comparten su runtime, pero una de React y otra de Vue arrastran dos. El presupuesto no cuenta solo tus componentes, también los motores que los animan.
Perseguir 100/100: qué mide cada métrica
El 100/100 se descompone en las tres Core Web Vitals, y cada una tiene una palanca clara en Astro.
De la métrica a la palanca hay un camino directo. El componente Image de Astro dimensiona y sirve formatos modernos, lo que ataca LCP y CLS a la vez: la imagen llega ligera y con su hueco reservado, así que no salta.
---import { Image } from 'astro:assets';import portada from '../assets/portada.jpg';---<!-- width y height reservan el espacio: cero salto de layout --><Image src={portada} alt="Portada" width={1200} height={630} loading="eager" />Las fuentes son la otra gran palanca. Servidas mal, bloquean el primer render o reflujan el texto al llegar; servidas bien —con la API de fuentes de Astro, que las autoaloja y las precarga— dejan de ser un lastre y se vuelven invisibles al usuario.
---import { Font } from 'astro:assets';---<head> <!-- autoaloja, precarga y aplica un fallback que no refluye el texto --> <Font cssVariable="--fuente-texto" preload /></head>Y lo que no se mide, se degrada. Tras cada build conviene mirar el peso real que llega al navegador, ruta por ruta, para cazar la isla que se coló en el presupuesto sin que nadie la invitara.
# El JS que de verdad viaja al cliente, ordenado por tamanonpx astro builddu -sh dist/_astro/*.js | sort -hLa trampa del 100/100 es creer que es un trofeo. Es un suelo: el punto desde el cual defiendes, no la cima que conquistas una vez. Astro te lo pone fácil porque parte de HTML estático y cero JS —el LCP y el INP salen bien casi solos—, pero cada isla que añades, cada fuente que cargas, cada imagen sin dimensionar, es una gota que baja la marea. Medir en el laboratorio diagnostica; medir con usuarios reales confirma.
Caché multinivel: CDN, islas y datos
El rendimiento no termina en el navegador: sigue hacia atrás en una cadena de cachés que se refuerzan. Diseñarla bien es servir casi todo sin recalcular casi nada.
El primer nivel es el CDN: el HTML estático y los assets con hash viven en el borde, cacheados globalmente y servidos sin tocar tu origen. Los assets con hash son inmutables, así que llevan una caché eterna; el HTML, una más corta con revalidación. El segundo nivel son las server islands: el fragmento dinámico se cachea aparte, con su propio TTL, de modo que la página sigue siendo estática y solo el trozo que late se refresca a su ritmo. El tercer nivel es el dato: la consulta cara se cachea en su origen —el caching incremental del Content Layer, una KV, una capa como Hyperdrive delante de tu base— con stale-while-revalidate para servir al instante lo último bueno mientras se recalcula por detrás.
Cada nivel se gobierna con una cabecera, y esa cabecera es donde el rendimiento se hace explícito. Un fragmento dinámico que tolera unos segundos de antigüedad puede declararlo y multiplicar por mil las peticiones que sirve sin tocar el origen:
// Un endpoint o una server island que se cachea unos segundos en el bordereturn new Response(cuerpo, { headers: { 'Cache-Control': 'public, s-maxage=30, stale-while-revalidate=300', },});