wandres.dev
NIVEL DIOS: SÍNTESIS · arquitectura y trade-offs

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í.

⏱ 22 min

Un 100/100 en Lighthouse no es un golpe de suerte ni una medalla que se cuelga al final: es un presupuesto que se defiende ruta a ruta, commit a commit. Astro te regala una ventaja que ningún meta-framework tiene: arrancas en cero JavaScript, así que la perfección es tu estado por defecto y el trabajo no es alcanzarla, sino no arruinarla. El rendimiento extremo tiene por eso dos frentes: vigilar cuánto JavaScript entra en cada ruta, y montar una caché de varios niveles que sirva casi todo sin recalcular casi nada.

🎯 Al terminar esta lección sabrás
  • Fijar un presupuesto de JavaScript por ruta y defenderlo en cada cambio.
  • Entender qué mide cada Core Web Vital y qué palanca lo mueve en Astro.
  • Diseñar una caché de tres niveles: CDN, server islands y datos.
  • Reconocer el coste real de cada isla para gastar el presupuesto con criterio.

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.

⚠️
La hidratación no es gratis

Una isla no cuesta solo los kilobytes que pesa: cuesta descargar su runtime, parsearlo, ejecutarlo y reconciliar el DOM que Astro ya había pintado. Ese trabajo ocurre en el hilo principal, el mismo que atiende los clics del usuario. Por eso cinco islas pequeñas pueden dañar la interactividad más que una imagen enorme: la imagen no compite por la CPU, la hidratación sí. Contar islas es contar coste, no adornos.

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.

🖼️

LCP

El mayor elemento visible. Se gana con HTML estático que llega pintado, el pipeline de imágenes con formatos modernos y dimensiones, y fuentes que no bloquean el render.

INP

La respuesta a la interacción. Depende del JavaScript en el hilo principal: menos islas y bien diferidas significan un hilo libre para responder al instante.

📐

CLS

El salto visual del layout. Se elimina reservando el espacio de imágenes y anuncios y evitando que las fuentes reflujen el texto al cargar.

🔬

Medición

Lighthouse en el laboratorio para diagnosticar, y RUM en producción para la verdad de usuarios reales. El 100/100 sintético es un suelo, no la meta.

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 tamano
npx astro build
du -sh dist/_astro/*.js | sort -h
📝
El presupuesto también cuenta lo que no es JavaScript

Es fácil obsesionarse con los kilobytes de JavaScript y olvidar que una fuente pesada, una imagen sin dimensionar o un script de terceros hunden las métricas igual de rápido. El presupuesto de una ruta no es solo su JS: es todo lo que el navegador debe descargar y procesar antes de estar listo. Cuenta las fuentes, los terceros y los píxeles, no solo tus islas.

La 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.

flowchart TD
RQ[Peticion del navegador] --> CDN[CDN sirve HTML y assets inmutables]
CDN -->|fragmento dinamico| ISL[Server island con su propio TTL]
ISL -->|dato fresco| DATA[Capa de datos KV o BD con cache]
DATA -->|stale while revalidate| ORIG[Origen recalcula en segundo plano]
style RQ fill:#89b4fa,color:#11111b
style CDN fill:#a6e3a1,color:#11111b
style ISL fill:#94e2d5,color:#11111b
style DATA fill:#f9e2af,color:#11111b

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 borde
return new Response(cuerpo, {
  headers: {
    'Cache-Control': 'public, s-maxage=30, stale-while-revalidate=300',
  },
});
🌍

Borde: CDN

HTML y assets inmutables servidos globalmente. Absorbe la mayoría del tráfico sin ejecutar nada de tu código.

🧩

Fragmento: server island

El trozo dinámico con su propio TTL. La página queda estática y solo el hueco fresco se recalcula a su ritmo.

🗄️

Dato: KV o BD

La consulta cara memorizada en su origen, con stale while revalidate para no bloquear nunca al usuario esperando el recálculo.

📈

Efecto: resiliencia

Cada capa absorbe carga antes de la siguiente, así que un pico de tráfico no se convierte en un pico de consultas.

💡
Cada nivel protege al siguiente

La virtud de la caché multinivel es que cada capa absorbe carga antes de que llegue a la de abajo. El CDN evita que la mayoría de peticiones toquen tu servidor; la server island evita que un render dispare tu base de datos; la caché de datos evita que un pico de tráfico se convierta en un pico de consultas. Un fallo o un pico se amortigua nivel a nivel en vez de propagarse hasta el origen. Diseñar las capas es diseñar la resiliencia, no solo la velocidad.

El rendimiento es una propiedad de la arquitectura, no un retoque final

La lección más profunda del rendimiento extremo es que la velocidad no se añade al final con un puñado de optimizaciones: se decide al principio, en la forma de la arquitectura, y luego solo se defiende. Astro encarna esta idea mejor que ningún otro framework porque invierte la carga de la prueba: donde otros arrancan cargando la aplicación entera y te piden que la adelgaces a base de esfuerzo, Astro arranca vacío —cero JavaScript, HTML estático, servido desde el borde— y te pide únicamente que no lo llenes sin pensar. La perfección es el estado inicial, y cada decisión posterior es un gasto que sale de un presupuesto. Por eso perseguir un 100/100 en Astro no se parece a escalar una montaña sino a cuidar un jardín: no hay una cima que se alcanza y ya está, hay un equilibrio que se mantiene rechazando la isla innecesaria, dimensionando la imagen, difiriendo la hidratación, cacheando el dato. Y la caché multinivel completa el cuadro con una verdad que trasciende Astro: un sistema rápido no es el que calcula deprisa, sino el que aprende a no calcular —el que reconoce que la petición de ahora se parece a la de hace un segundo y se niega a repetir el trabajo—. CDN, isla y dato son tres formas de la misma sabiduría: recordar en vez de rehacer. Cuando interiorizas que el rendimiento es estructura y no cosmética, dejas de perseguir números al final del proyecto y empiezas a merecerlos desde el primer commit, porque cada pieza estaba, desde el diseño, en el lugar que la hacía rápida.

⚔️ Defiende un presupuesto y una caché
  1. Fija un presupuesto de JavaScript para tres rutas distintas de un sitio —contenido, marketing y aplicación— con un número de KB para cada una.
  2. Audita una página real con Lighthouse y atribuye cada punto perdido a una métrica —LCP, INP o CLS— y a su causa concreta.
  3. Convierte una página que hoy es dinámica entera en una cáscara estática con una server island para el único dato que de verdad late, y ponle una cabecera de caché con su TTL.
  4. Dibuja los tres niveles de caché de tu sitio —CDN, isla y dato— y señala qué política de revalidación pondrías en cada uno.