El rol de Vite en Astro: dev server y build
Vite es el motor que Astro monta por debajo, y vive dos vidas distintas: un dev server que sirve ESM nativo al navegador sin empaquetar, con HMR casi instantáneo, y un pipeline de build que sí empaqueta para producción. Dónde encaja el compilador de Astro escrito en Rust como plugin de la cadena de transformación, por qué desarrollar y construir siguen caminos opuestos y cómo se reconcilian en Astro 7 sobre Vite 8.
Las dos vidas de Vite
La palabra clave para entender a Vite es asimetría. Durante el desarrollo, tu prioridad es la latencia: quieres que un cambio se refleje antes de que apartes la vista del editor. Durante el build, tu prioridad es la eficiencia del artefacto: quieres el menor número de bytes, la menor cantidad de peticiones y el HTML más limpio que el navegador del visitante pueda descargar. Esas dos prioridades tiran en direcciones contrarias, y Vite responde con dos estrategias radicalmente distintas bajo la misma configuración.
Esta separación no es un accidente histórico, sino la tesis central de Vite. Las herramientas anteriores empaquetaban también en desarrollo, y por eso su arranque crecía sin techo con el tamaño del proyecto: cada pulsación de guardar disparaba un reempaquetado del mundo. Vite rompió esa proporcionalidad apoyándose en algo que el navegador moderno ya sabe hacer solo.
La regla que gobierna las dos vidas se resume en una frase: paga el coste donde es barato.
- En desarrollo, la red es local y las peticiones son casi gratis, así que Vite prefiere muchas peticiones pequeñas a un empaquetado costoso que retrase cada guardado.
- En producción, la red del visitante es lenta y cara, así que Vite prefiere un empaquetado costoso —pagado una sola vez en el build— a obligar al navegador a cientos de peticiones.
El dev server: ESM nativo y transformación bajo demanda
El navegador entiende import y export de forma nativa desde hace años. Vite explota ese hecho al máximo: en desarrollo no genera un bundle, sino que arranca un servidor que responde a cada import con el módulo correspondiente, transformado al vuelo. Pides la página, el navegador ve un import a un .astro, lo solicita, Vite lo transforma en ese instante y lo devuelve como JavaScript válido. Cada módulo es una petición, y solo se transforma lo que de verdad se carga.
Queda un problema: las dependencias de node_modules. Muchas se publican todavía en formato CommonJS, que el navegador no entiende, y otras están fragmentadas en cientos de ficheros diminutos que dispararían una cascada de peticiones fatal. Por eso Vite hace un paso previo llamado pre-bundling o optimizeDeps: convierte cada dependencia a ESM y colapsa sus muchos ficheros internos en uno solo. Históricamente lo hacía esbuild; en Vite 8 ese trabajo lo asume Rolldown, el mismo motor que empaqueta la producción.
Ese pre-empaquetado resuelve dos problemas a la vez, y por eso es imprescindible aunque el resto de tu código se sirva sin empaquetar:
- Convierte a ESM las dependencias publicadas en CommonJS, para que el navegador pueda importarlas de forma nativa sin un paso de traducción en cada carga.
- Colapsa los muchos ficheros internos de una librería en uno, evitando que un solo
importdesate una cascada de cientos de peticiones encadenadas.
# el arranque del dev server incluye la fase de pre-empaquetadoastro dev# vite pre-empaqueta las dependencias detectadas una sola vez y las cachea# luego sirve tu codigo modulo a modulo, sin volver a empaquetar nadaDónde encaja el compilador de Astro
Aquí aparece la pieza propia de Astro. Un fichero .astro no es JavaScript válido: mezcla un frontmatter de servidor, marcado tipo HTML con expresiones y estilos con scope. El navegador no sabe nada de esa sintaxis, y Vite tampoco de fábrica. Lo que traduce ese dialecto es el compilador de Astro, reescrito en Rust en Astro 7, y se inserta en la tubería como un plugin de Vite.
Un plugin de Vite es, en esencia, un objeto con hooks que interceptan el ciclo de un módulo. El hook transform recibe el código fuente de un fichero y devuelve el código transformado. El plugin de Astro registra un transform que reconoce las extensiones .astro, entrega su contenido al compilador y recibe de vuelta tres productos de ese único fichero:
- Un módulo JavaScript con las funciones de render que producen el HTML del componente.
- Los estilos con scope extraídos, listos para que Vite los sirva sueltos o los empaquete aparte.
- La lista de islas detectadas, con sus directivas
client:, para saber qué habrá que hidratar en el navegador.
// forma simplificada del plugin que integra el compiladorexport function astroPlugin() { return { name: 'astro', async transform(code, id) { if (!id.endsWith('.astro')) return null; const resultado = await compilar(code, id); // compilador en Rust return { code: resultado.js, map: resultado.map }; }, };}Lo esencial es el orden mental: Vite gobierna la orquestación —qué módulo se pide, cuándo, con qué caché— y el compilador de Astro gobierna la traducción de un dialecto concreto. Vite no sabe qué es una isla; el compilador no sabe qué es el HMR. Cada uno hace su parte y se comunican por el contrato mínimo del hook transform. La misma tubería, con el mismo plugin, atiende tanto al dev server como al build: la diferencia no está en cómo se traduce un .astro, sino en qué se hace después con el módulo ya traducido.
Esa última idea encierra una simetría útil: el compilador de Astro produce exactamente el mismo módulo en desarrollo y en producción. Lo que difiere es el destino de ese módulo —servido suelto al navegador o empaquetado en un chunk— pero no su traducción. Por eso un componente que se ve bien en astro dev casi siempre se comporta igual en el build: la fuente de la verdad, el compilador, es una sola, y solo cambia la maquinaria que consume su salida.
HMR: el grafo que se actualiza en caliente
El dev server mantiene en memoria un grafo de módulos: un mapa de quién importa a quién. Cuando guardas un fichero, Vite no recompila el proyecto; consulta el grafo, localiza los módulos afectados y propaga la invalidación hacia arriba hasta encontrar una frontera capaz de aceptar el cambio en caliente. Solo esos módulos se retransmiten al navegador.
Ese grafo en memoria guarda, para cada módulo, más de lo que parece:
-
Sus importadores y sus importados, las aristas que permiten propagar una invalidación en ambos sentidos.
-
El resultado ya transformado en caché, para no recompilar lo que no ha cambiado.
-
Si el módulo define una frontera de HMR, el dato que decide dónde se detiene la propagación.
-
Editar el cuerpo de un componente de framework con estado suele actualizarse preservando el estado, porque su integración define una frontera de HMR.
-
Editar una página
.astrocompleta provoca a menudo una recarga de la ruta, porque el HTML del servidor cambia entero. -
Editar un módulo de datos sin frontera propia propaga la invalidación a quien lo importa, hasta topar con una que sí la tenga.
Ese concepto de frontera es lo que hace posible preservar el estado. Una integración de framework declara, a través de la API import.meta.hot, que sabe aceptar una versión nueva de un componente sin recrearlo desde cero; ahí se detiene la propagación y el estado local sobrevive. Un .astro de página, en cambio, rara vez define esa frontera, porque su salida es HTML de servidor que hay que volver a pedir entero. La diferencia de comportamiento que observas al editar no es arbitraria: es el mapa de qué módulos saben renacer en caliente y cuáles deben volver a nacer desde el servidor.
# el dev server informa de cada actualizacion en calienteastro dev# [vite] hmr update /src/components/Contador.tsx# no reconstruye el proyecto: retransmite solo el modulo tocado