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.
Astro no reinventa el sistema de módulos ni el empaquetado: los delega en Vite, la herramienta que corre debajo de cada astro dev y cada astro build. Pero Vite no es una sola cosa. Es dos motores con filosofías opuestas cosidos bajo una API común: uno que sirve tu código sin tocarlo apenas mientras desarrollas, y otro que lo tritura y reempaqueta para producción. Entender esa dualidad —y saber en qué punto exacto se inserta el compilador de Astro— es dejar de ver el build como una caja negra y empezar a leerlo como una tubería con etapas nombradas.
- Distinguir las dos vidas de Vite: el dev server sin empaquetar y el build que sí empaqueta.
- Entender por qué el desarrollo sirve ESM nativo y transforma bajo demanda en lugar de construir todo.
- Situar el compilador de Astro como un plugin de Vite dentro de la cadena de transformación.
- Ver cómo el HMR actualiza el grafo de módulos en caliente sin reconstruir el proyecto.
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.
Dev server
No empaqueta. Sirve cada módulo como ESM nativo y lo transforma solo cuando el navegador lo pide. El coste es proporcional a lo que cambias, no al tamaño del proyecto.
Build
Sí empaqueta. Recorre el grafo completo, aplica tree shaking, divide en chunks y minifica. El coste es proporcional al proyecto entero, pero se paga una vez por despliegue.
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 arranque de un servidor basado en empaquetado crece con el tamaño del proyecto, porque debe construirlo entero antes de servir la primera página. El de Vite es casi constante: solo pre-empaqueta las dependencias y sirve tu código bajo demanda, así que abrir un proyecto de diez mil ficheros tarda parecido a abrir uno de diez. Esa independencia entre tamaño y latencia de arranque es la promesa práctica que justifica toda la arquitectura sin empaquetado en desarrollo.
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.
flowchart LR BROWSER[navegador pide un modulo] --> DEV[vite dev server] DEV --> PLUGIN[plugin compila el fichero pedido] PLUGIN --> ESM[modulo ESM transformado] ESM --> BROWSER DEV -. nunca empaqueta en dev .-> NOTE[cada import es una peticion] style DEV fill:#89b4fa,color:#11111b style ESM fill:#a6e3a1,color:#11111b
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-empaquetado
astro dev
# vite pre-empaqueta las dependencias detectadas una sola vez y las cachea
# luego sirve tu codigo modulo a modulo, sin volver a empaquetar nada
Vite trata tu código fuente y tus dependencias con dos varas distintas, y la razón es la frecuencia de cambio. Tu código cambia a cada segundo: pre-empaquetarlo sería tirar el trabajo constantemente, así que se sirve módulo a módulo. Tus dependencias casi nunca cambian: pre-empaquetarlas una vez y cachear el resultado es gratis a partir de la segunda carga. Esa frontera entre lo mutable y lo estable es la que decide qué se optimiza por adelantado y qué se transforma bajo demanda.
Dó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 compilador
export 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 caliente
astro dev
# [vite] hmr update /src/components/Contador.tsx
# no reconstruye el proyecto: retransmite solo el modulo tocado
Cuando un cambio parece recargar de más, casi siempre es que la invalidación subió por el grafo sin encontrar una frontera de HMR antes de llegar a la raíz. Saber esto convierte la frustración en diagnóstico: si un cambio pequeño recarga la página entera, pregúntate qué módulo intermedio carece de frontera y fuerza a propagar hasta arriba. El comportamiento no es aleatorio; es la geometría de tus imports.
Hay una tentación natural de creer que el build de producción es simplemente el dev server ejecutado hasta el final, como si construir fuera desarrollar en serio. Es exactamente al revés, y comprender por qué reordena toda tu intuición sobre las herramientas modernas. El dev server y el build no son dos intensidades de lo mismo: son dos respuestas a dos preguntas incompatibles. El dev server pregunta cómo reflejar un cambio con la menor latencia posible, y su respuesta óptima es no empaquetar, servir módulos sueltos y dejar que el navegador reconstruya el grafo en cada carga, pagando el coste en peticiones porque en local esas peticiones son gratis. El build pregunta cómo entregar el sitio a un desconocido al otro lado de una red lenta, y su respuesta óptima es la contraria: empaquetar agresivamente, eliminar todo lo no usado, dividir en trozos cacheables y minimizar cada byte, pagando el coste en tiempo de construcción porque ese tiempo se amortiza sobre miles de visitantes. Son óptimos opuestos porque optimizan variables opuestas —latencia de iteración frente a eficiencia de entrega— y ninguna herramienta que sirviera a las dos con la misma estrategia podría ser buena en ninguna. Vite es célebre precisamente por negarse a ese compromiso: en lugar de un motor mediocre para ambos casos, mantiene dos motores excelentes bajo una configuración compartida, y su historia reciente es la de reducir la distancia entre ellos sin fusionarlos. Rolldown en Vite 8 es ese movimiento: el mismo bundler nativo pre-empaqueta dependencias en desarrollo y empaqueta la producción, así que lo que ves al desarrollar y lo que despliegas divergen menos que nunca, aunque sigan siendo respuestas distintas. Astro se coloca encima de esa dualidad sin ocultarla: aporta el compilador que traduce su dialecto y deja que Vite decida qué hacer con el resultado según la pregunta que se esté haciendo. Aprender a ver esa frontera —dónde termina la traducción de Astro y dónde empieza la orquestación de Vite, y por qué la misma traducción alimenta dos estrategias opuestas— es aprender a razonar sobre cualquier build moderno, no solo el de Astro.
- Arranca
astro dev, abre la pestaña de red del navegador y comprueba que cada módulo llega como una petición ESM separada, sin empaquetar. - Edita el texto de un componente y localiza en la terminal la línea
hmr update; confirma que menciona solo el fichero tocado. - Edita ahora una página
.astroentera y observa la diferencia: la ruta se recarga en lugar de actualizarse en caliente. - Ejecuta
astro buildy compara el contenido de la carpeta de salida con lo que servía el dev server: verás ficheros empaquetados y con hash, no módulos sueltos.