wandres.dev
ARQUITECTURA DE ISLAS · client:load, visible, only

client:visible y client:media: hidratar bajo demanda

Las dos directivas perezosas: client:visible difiere la hidratación hasta que la isla entra en el viewport mediante IntersectionObserver, con un rootMargin opcional para adelantarla; client:media la condiciona a una media query, de modo que una isla solo de móvil jamás carga su JavaScript en escritorio. Ambas comparten una virtud —el coste que nunca se paga— que las convierte en la palanca de rendimiento más potente.

⏱ 14 min

client:load y client:idle acaban hidratando siempre; la única pregunta era cuándo. Las dos directivas de este capítulo introducen algo más ambicioso: una isla que quizá no se hidrate nunca, y que por tanto quizá no descargue jamás su JavaScript. client:visible la condiciona a que entre en pantalla; client:media, a que se cumpla una media query. Cuando la condición no llega, el coste no se paga. Esa es la forma más pura del ahorro que promete Astro.

🎯 Al terminar esta lección sabrás
  • Diferir la hidratación con client:visible y el IntersectionObserver.
  • Ajustar el adelanto con el rootMargin opcional.
  • Condicionar la hidratación a una media query con client:media.
  • Entender por qué el coste que no se paga es la palanca más potente.

client:visible: cuando entra en pantalla

client:visible difiere la hidratación hasta que la isla aparece en el viewport. Por debajo usa un IntersectionObserver, la API del navegador que avisa cuando un elemento cruza el borde de lo visible, sin coste de sondeo continuo. Mientras la isla siga fuera de pantalla, su framework y su componente ni siquiera se descargan; el observador vigila, y solo cuando el usuario llega con el scroll se dispara la importación y la hidratación.

---
import Comentarios from '../components/Comentarios.jsx';
---
<Comentarios client:visible />

Es la directiva natural para casi todo lo que vive bajo el pliegue:

  • Una sección de comentarios al final de un artículo.
  • Un carrusel o una galería a media página.
  • Un mapa o un vídeo incrustado en el pie.
  • Cualquier widget pesado que el usuario quizá ni alcance.

En una página larga, la mayoría de las islas caen aquí, y el efecto agregado es enorme: el navegador arranca sin descargar el JavaScript de nada que no esté a la vista, y lo va trayendo a medida que el lector avanza, repartido en el tiempo en lugar de amontonado en el arranque. client:visible acepta además un ajuste fino a través de rootMargin, que se pasa directo al observador: client:visible={{rootMargin: "200px"}} empieza a hidratar cuando la isla está a doscientos píxeles de entrar, no cuando ya asoma. Ese margen esconde la latencia de descarga tras el scroll, de modo que la isla llega hidratada justo cuando el usuario la alcanza.

---
import Mapa from '../components/Mapa.jsx';
---
<Mapa client:visible={{rootMargin: "200px"}} />

El observador por dentro

Un IntersectionObserver no sondea la posición en un bucle; el navegador le notifica cuando el elemento cruza un umbral, de forma pasiva y barata. Astro crea uno por isla client:visible, lo dispara en la primera intersección y lo desconecta acto seguido: la hidratación ocurre una sola vez y el observador no queda vigilando para siempre. El único parámetro que la directiva expone es rootMargin, el colchón alrededor del viewport que adelanta o retrasa el disparo; el resto de la maquinaria queda oculto tras la directiva, que es justo el nivel de abstracción que quieres manejar a diario.

📝
No pongas client:visible sobre el pliegue

Si una isla ya está en pantalla al cargar, client:visible no ahorra nada: el observador dispara de inmediato y solo añade una capa de indirección. Para lo que se ve al entrar, client:load o client:idle expresan mejor la intención. client:visible da su fruto exactamente cuando la isla empieza fuera de la vista y hay una probabilidad real de que el usuario no llegue a ella.

ℹ️
Hidratar bajo demanda no es cargar imágenes en diferido

Conviene no confundir client:visible con el loading="lazy" nativo de imágenes e iframes. Ambos difieren trabajo hasta que algo se acerca a la vista, pero operan en planos distintos: loading="lazy" retrasa la descarga de un recurso estático que el navegador gestiona solo, mientras que client:visible retrasa la hidratación de una isla, es decir, la ejecución del JavaScript que reanima un componente. Son complementarios: una misma página puede diferir sus imágenes con el atributo nativo y sus islas con la directiva, cada uno en su capa.

client:media: cuando la pantalla lo pide

client:media condiciona la hidratación a una media query CSS. La isla solo se hidrata si la consulta se cumple. El caso canónico es el componente que solo existe en un tamaño de pantalla: un menú de hamburguesa que únicamente aparece en móvil, o un panel lateral que solo tiene sentido en escritorio.

---
import MenuMovil from '../components/MenuMovil.jsx';
---
<MenuMovil client:media="(max-width: 768px)" />

Aquí el ahorro es tajante y binario. En un escritorio ancho, esa media query nunca se cumple, así que el JavaScript del menú móvil jamás se descarga en ese dispositivo: no es que se difiera, es que no llega. Servir el widget correcto a cada clase de pantalla, sin cargar el del otro lado, es algo que ninguna cantidad de client:visible consigue, porque la condición no es la posición sino el medio. Casos donde client:media brilla:

  • Un menú de hamburguesa que solo existe en móvil.
  • Un panel lateral o una tabla ancha propios de escritorio.
  • Controles táctiles que no tienen sentido con un ratón.
  • Cualquier isla cuyo JavaScript sobra en media clase de pantallas.
⚠️
El HTML de client:media sigue ahí

client:media gobierna la hidratación, no el renderizado de servidor. La isla se renderiza a HTML en el servidor igual que cualquier otra, y ese HTML se envía siempre, cumpla o no la media query. Lo que la query decide es si ese marcado llega a cobrar vida con JavaScript. Si no quieres ni el HTML en cierto tamaño, eso es trabajo de CSS —ocultar con display: none— o de renderizado condicional en el servidor, no de la directiva.

💡
media reacciona a los cambios de viewport

client:media no solo mira la query al cargar: Astro observa los cambios de tamaño, así que si el usuario gira el móvil o redimensiona la ventana y la query pasa a cumplirse, la isla se hidrata en ese momento. Lo que no debes hacer es duplicar el mismo componente con dos media queries complementarias para “cubrir ambos lados”: eso envía las dos versiones. Si el componente vale para todos los tamaños, no es un caso de client:media, sino de client:visible o client:idle.

El coste que no se paga

Lo que hermana a estas dos directivas es que ambas pueden resolverse en no hacer nada. Una isla client:visible que el usuario nunca alcanza con el scroll, o una client:media cuya query nunca casa, no descargan código, no ocupan el hilo principal y no cuestan más que su HTML ya servido. El mejor JavaScript es el que nunca se envía, y estas directivas lo consiguen no por optimizar la carga, sino por condicionarla a una demanda real.

👁️

client:visible

Hidrata al entrar en el viewport vía IntersectionObserver. Para todo lo que vive bajo el pliegue.

📐

rootMargin

client:visible={{rootMargin: "200px"}} adelanta la hidratación para esconder la latencia tras el scroll.

📱

client:media

Hidrata solo si una media query se cumple. Para widgets exclusivos de un tamaño de pantalla.

🚫

Coste cero condicional

Si la condición no llega, no hay descarga ni hidratación: solo el HTML que ya viajaba.

flowchart TB
ISLA[isla marcada perezosa] --> V{client visible}
V -->|fuera de pantalla| NADA[cero JS descargado]
V -->|entra en el viewport| HYD[el observer dispara e hidrata]
ISLA --> M{client media}
M -->|la query no casa| NADA
M -->|la query casa| HYD
style NADA fill:#a6e3a1,color:#11111b
style HYD fill:#89b4fa,color:#11111b
style ISLA fill:#f9e2af,color:#11111b

Puestas en fila, las cuatro directivas de carga se distinguen por dos ejes: cuándo disparan y si pueden no disparar jamás.

  • client:load — al llegar el código; siempre hidrata.
  • client:idle — en el primer respiro del hilo; siempre hidrata.
  • client:visible — al entrar en pantalla; puede no hidratar nunca.
  • client:media — si la query casa; puede no hidratar nunca.

Un reparto realista

Imagina un artículo largo con una cabecera que busca, comentarios al final y un menú solo de móvil. Un reparto sensato de directivas sería:

  • La cabecera con búsqueda: client:idle, útil pero no urgente.
  • Los comentarios del pie: client:visible, que casi nadie despliega.
  • El menú de móvil: client:media, ausente por completo en escritorio.

Ninguna isla usa client:load, y sin embargo la página se siente instantánea, porque el coste se reparte según lo que cada visitante hace de verdad. Ese reparto es lo que hace que el JavaScript de la página crezca con lo que el usuario usa, no con lo que el autor escribió.

Lo revelador llega al medir en campo: la mayoría de las sesiones hidratan solo una fracción de las islas de la página, porque casi nadie agota el recorrido. Diseñar para ese hecho —y no para el visitante hipotético que lo toca todo— es lo que separa una página que se siente rápida de una que solo lo parece en el laboratorio.

Condicionar la carga a la demanda invierte la pregunta del rendimiento

Durante años, optimizar la carga de una web consistió en hacer más barato lo que ya se enviaba: minificar, comprimir, trocear, cargar en paralelo. Todas esas técnicas aceptan una premisa que nunca cuestionan —que el código se va a enviar— y trabajan sobre el cómo. client:visible y client:media atacan un escalón por encima, sobre el si: no abaratan la descarga, la condicionan a que exista una demanda. Y esa demanda la revela el propio usuario con sus actos —al hacer scroll, al usar cierto tamaño de pantalla— en lugar de decidirla el desarrollador por adelantado. La consecuencia es profunda. En el modelo clásico, el coste de una página es la suma de todo lo que contiene, fijada en el build; da igual qué haga el visitante, paga por el conjunto. En el modelo de demanda, el coste de una página es una variable que cada visitante determina con su comportamiento: quien solo lee el primer párrafo apenas paga; quien recorre el artículo entero paga isla a isla, a medida que las descubre. El rendimiento deja de ser una propiedad estática del documento y pasa a ser una función del recorrido real. Interiorizar esto reordena tus decisiones: dejas de preguntarte “¿cómo hago más ligero esto que envío?” y empiezas a preguntarte “¿tengo siquiera que enviarlo, o puedo esperar a que alguien lo pida?”. La mayoría de las veces —y esta es la lección incómoda para quien viene del todo-al-cliente— la respuesta es que puedes esperar, y que casi nadie llega a pedirlo.

⚔️ Condiciona la carga a la demanda
  1. Coloca una isla pesada al final de una página larga con client:visible y confirma en la pestaña de red que su JS no se descarga hasta que llegas a ella con el scroll.
  2. Añade rootMargin y observa cómo la descarga se adelanta antes de que la isla asome.
  3. Marca un menú con client:media="(max-width: 768px)" y comprueba en escritorio que su JavaScript nunca llega.
  4. Reduce la ventana por debajo del umbral y verifica que ahora sí se hidrata: la demanda cambió, el coste apareció.