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