El presupuesto de JavaScript: medir, minimizar, decidir
Convertir la intuición de enviar poco JavaScript en una disciplina medible: leer cuánto pesa cada isla en la salida del build y en la red, minimizar la hidratación decidiendo qué merece ser isla y con qué framework, y elegir entre client:visible y client:only con criterio de coste, SSR y estabilidad visual.
Enviar poco JavaScript es fácil de predicar y difícil de gobernar. La intuición no basta: un bundle crece de a poco, una dependencia inocente arrastra doscientos kilobytes, y una isla marcada client:load sin pensarlo carga en el arranque lo que el visitante quizá nunca toque. Este capítulo convierte aquella intuición en disciplina de ingeniería: aprender a leer cuánto pesa cada isla, a minimizar lo que se hidrata y a decidir la directiva con un criterio explícito. Un presupuesto no es un eslogan; es una cifra que se mide, se vigila y se defiende en cada cambio.
- Leer el peso de cada isla en la salida del build y en la red.
- Minimizar la hidratación decidiendo qué merece de verdad ser isla.
- Elegir framework y dependencias con el presupuesto delante.
- Distinguir con criterio cuándo usar
client:visibley cuándoclient:only.
Medir cuánto pesa cada isla
No se administra lo que no se mide, y el primer instrumento lo trae Astro de fábrica: la propia salida del build. Cada trozo de cliente que emite aparece en la terminal con su tamaño en crudo y comprimido con gzip, que es lo que de verdad viaja por la red. Ese informe es tu extracto de cuenta.
npm run build
# Astro lista cada trozo de cliente con su tamano
# dist/_astro/Comentarios.a1b2c3.js 42.10 kB | gzip: 14.80 kB
# dist/_astro/client.e4f5g6.js 9.30 kB | gzip: 3.70 kB
La columna que importa es la de gzip: un componente de cuarenta kilobytes que se comprime a quince cuesta quince en la red. Lee esa lista como un balance —cada línea es un cargo— y asocia cada trozo a la isla que lo originó por su nombre. Cuando un chunk sorprende por su peso, casi siempre esconde una dependencia dentro, no el componente en sí.
Para ver qué infla un trozo, un visualizador de bundle desglosa el interior en un mapa de áreas donde cada rectángulo es un módulo y su superficie es proporcional a lo que aporta.
npx vite-bundle-visualizer
# genera un treemap: el rectangulo mas grande suele ser
# una dependencia, no tu propio codigo
La cifra del build es el coste de transferencia, pero el coste real de una isla incluye el tiempo de CPU de hidratarla, que no se ve en kilobytes. Completa la foto con la pestaña de rendimiento del navegador: graba la carga, busca las tareas largas del hilo principal y comprueba cuáles nacen al hidratar. Un componente ligero de descargar puede aun así bloquear el hilo si su hidratación recorre un DOM grande. Peso transferido y peso de ejecución son dos cargos distintos de la misma factura.
Minimizar la hidratación
Con la medición en la mano, minimizar es actuar sobre tres palancas ordenadas por impacto. La primera y más rentable es cuestionar la existencia de la isla: mucho de lo que se marca interactivo se resuelve con HTML y CSS —un details nativo, un enlace ancla, una animación por CSS— sin una gota de JavaScript. La isla más barata sigue siendo la que no existe.
La segunda palanca es el framework. Ya viste en el nivel de islas que Preact, Solid o Svelte pesan una fracción de React; en un presupuesto ajustado, esa elección se paga en cada visita y es de las que más rinde. La tercera es la dependencia: una librería de fechas de cien kilobytes importada dentro de una isla para formatear una hora cuesta eso, se use o no, y suele ser el rectángulo grande del treemap.
---
// Antes: una isla solo para desplegar contenido
import Acordeon from '../components/Acordeon.jsx';
---
<Acordeon client:visible />
<!-- Despues: cero JavaScript, con un elemento nativo -->
<details>
<summary>Ver detalles</summary>
<p>Contenido que ya estaba en el HTML.</p>
</details>
El error más caro no es tener demasiadas islas, sino meter una librería voluminosa dentro de una. Ese peso se descarga cuando la isla se hidrata, y ninguna directiva perezosa lo abarata: solo lo aplaza. Antes de importar una dependencia en código de cliente, pregúntate si su trabajo puede hacerse en el servidor —donde no cuesta nada al visitante— o resolverse con una API nativa del navegador. Mover el cómputo al servidor es, casi siempre, la optimización de mayor retorno.
client:visible frente a client:only
Las directivas ya las conoces; lo que este nivel exige es elegir entre las dos que más se confunden. client:visible renderiza la isla a HTML en el servidor y difiere solo su hidratación hasta que entra en pantalla: el visitante ve el marcado desde el primer instante y el JavaScript llega después. client:only hace algo distinto y más drástico: omite el renderizado de servidor por completo. El componente no produce HTML; el navegador lo monta desde cero tras descargar su código.
---
import Grafica from '../components/Grafica.jsx';
---
<div class="reserva-grafica">
<Grafica client:only="react" />
</div>
<style>
/* Reserva el alto para que la grafica no empuje el layout al montar */
.reserva-grafica { min-block-size: 320px; }
</style>
El criterio es claro. Prefiere siempre client:visible: da HTML de servidor, mejor LCP y ninguna sorpresa de layout. Reserva client:only para lo que no puede renderizarse en el servidor —un componente que lee window o document en su cuerpo, o depende de una librería que solo existe en el navegador—. Cuando lo uses, recuerda dos cosas: nombrar el framework (client:only="react") porque Astro no lo infiere sin SSR, y reservar el espacio con CSS para no provocar un salto visual cuando la isla aparezca de golpe.
Al no renderizar en el servidor, una isla client:only deja un hueco vacío en el HTML inicial. Quien llegue con el JavaScript deshabilitado, con una red lenta o siendo un rastreador que no ejecuta scripts, no verá nada en ese lugar. No es un defecto, es el trato: renuncias al HTML de servidor a cambio de poder usar código que solo el navegador entiende. Asúmelo como decisión consciente y limítalo a los componentes que de verdad lo necesitan.
El presupuesto como contrato, no como buena intención
Un límite que solo vive en la cabeza del autor se erosiona en el primer día de prisa. Para que el presupuesto sobreviva, hay que convertirlo en una comprobación automática que falle el build cuando se rebase, igual que un test que se pone rojo. Herramientas como size-limit fijan un techo por entrada y lo verifican en cada integración.
# Falla la comprobacion si una entrada supera su techo
npx size-limit
# index: 48.2 kB (limite 50 kB) OK
# articulo: 63.1 kB (limite 50 kB) EXCEDIDO
Con esa red puesta, el presupuesto deja de depender de la disciplina individual y pasa a ser una propiedad del proyecto que nadie rompe sin enterarse. La conversación cambia de tono: en vez de descubrir semanas después, en las métricas de campo, que una página engordó, el autor lo ve en la revisión, cuando aún es barato revertirlo. Un presupuesto vigilado por la máquina es lo que separa una intención de una garantía.
El build es el extracto
Cada isla aparece con su tamaño gzip en la salida del build. Léelo como un balance de cargos.
El treemap acusa
Un visualizador de bundle muestra qué dependencia infla cada trozo. El rectángulo grande es el culpable.
Menos, más ligero
Primero cuestiona la isla, luego elige el framework liviano, luego destierra la dependencia pesada.
visible por defecto
client:visible da SSR y difiere; client:only solo para lo que el navegador exige, con espacio reservado.
flowchart TD
Q0{necesita interactividad real} -->|no| HTML[HTML y CSS cero JS]
Q0 -->|si| Q1{puede renderizarse en el servidor}
Q1 -->|si y es critico arriba| LOAD[client load]
Q1 -->|si pero puede esperar| VIS[client visible o idle]
Q1 -->|no solo vive en el navegador| ONLY[client only con espacio reservado]
style HTML fill:#a6e3a1,color:#11111b
style VIS fill:#89b4fa,color:#11111b
style LOAD fill:#f9e2af,color:#11111b
style ONLY fill:#fab387,color:#11111bHay un cambio de mentalidad que separa a quien optimiza de verdad de quien solo reacciona a una mala nota de Lighthouse, y cabe en una frase: el JavaScript que envías no es una funcionalidad que sumas, es una deuda que contraes. Cada kilobyte que cruza la red hay que descargarlo con la batería del visitante, analizarlo con su procesador, ejecutarlo en su único hilo principal y mantenerlo vivo en su memoria; y todo eso ocurre en dispositivos que tú no controlas, muchos de ellos modestos, en redes que tú no elegiste. Un presupuesto de JavaScript es, entonces, mucho más que un techo de kilobytes: es la decisión de tratar ese recurso como lo que es, un pasivo que solo se justifica cuando el uso real lo amortiza. Por eso la disciplina empieza en la medición y no en la optimización. No puedes amortizar lo que no cuantificas, ni defender un límite que nunca escribiste. El desarrollador que interioriza esto invierte el orden habitual de las preguntas: no parte de un framework y busca dónde encajarlo, sino que parte del contenido —que casi siempre es HTML— y se pregunta, isla por isla, si añadir código de cliente compra suficiente valor como para pagar su intereses en cada visita. Medir cada isla, minimizar lo que se hidrata, elegir la directiva con criterio: no son tres tareas sueltas, son tres momentos de una misma contabilidad, la que trata el rendimiento no como un ajuste final sino como una restricción de diseño presente desde la primera línea. Cuando el presupuesto se vuelve un hábito, deja de sentirse como una limitación y empieza a sentirse como lo que es: la forma honesta de construir para gente cuyos dispositivos no son el tuyo.
- Ejecuta
npm run builden un proyecto con varias islas y anota el tamañogzipde cada trozo de cliente que aparezca en la lista. - Corre un visualizador de
bundley localiza la dependencia que más área ocupa dentro de tu isla más pesada. - Toma una isla que solo despliegue o muestre contenido y reescríbela con HTML y CSS nativos; vuelve a construir y compara el presupuesto.
- Convierte una isla
client:loadno crítica enclient:visibley una que usewindowenclient:onlycon espacio reservado; verifica en la red qué se difiere y qué desaparece del HTML inicial.