Qué es Vite y por qué#
Ya conoces el bundler y el dev server del capítulo puente: un bundler junta y optimiza tus módulos, y el dev server arranca un servidor local con recarga en caliente mientras programas. Vite hace las dos cosas, pero de una forma que lo hace notablemente más rápido que sus predecesores.
Antes de Vite, la herramienta estándar era Webpack, el bundler que dominó el sector durante años. Webpack construye un grafo de módulos completo cada vez que arrancas: procesa todos los ficheros, resuelve todos los imports y genera un bundle antes de servir la primera página. En proyectos grandes eso se traduce en esperas de 30-60 segundos solo para ver la primera pantalla.
Vite, diseñado por Evan You (el creador de Vue), cambia la estrategia por completo. El resultado es un arranque casi instantáneo y actualizaciones que llegan al navegador en menos de un segundo.
El dev server: arranque instantáneo#
La clave está en cómo Vite sirve los ficheros en desarrollo.
Los navegadores modernos soportan ESM nativo: pueden cargar módulos directamente con <script type="module"> sin necesidad de que nadie los empaquete antes. Vite aprovecha esto:
// El navegador hace esta petición directamente:
// GET /src/main.js
// src/main.js — tu código tal cual, Vite solo añade transformaciones mínimas
// el navegador pide también /src/datos/heroes.js
import { heroes } from './datos/heroes.js';
// y /src/ui/tabla.js, y así hasta el final
import { renderTabla } from './ui/tabla.js';
// ejecuta la función que muestra los héroes en pantalla
renderTabla(heroes);Cuando pides localhost:5173 en el navegador, Vite sirve el index.html tal cual. El navegador lee el <script type="module">, pide /src/main.js, y Vite solo transforma ese fichero concreto en el momento en que se pide. No hay bundle previo. El tiempo de arranque es independiente del tamaño del proyecto.
HMR: guardar y ver al instante#
Cuando guardas un fichero, Vite no recarga la página entera. En su lugar, usa HMR (Hot Module Replacement): sustituye solo el módulo que cambió en el navegador que ya tienes abierto.
La diferencia práctica:
- Sin HMR: guardas, la página recarga, navegas de nuevo al estado que tenías.
- Con HMR: guardas, el cambio aparece en 50-100 ms y normalmente sin perder el estado de la página.
Si estás probando el comportamiento de un héroe concreto en el Team Builder, no tienes que volver a buscarlo cada vez que ajustas un estilo o un cálculo.
Por qué vuela: Rolldown#
La velocidad de Vite no es solo la estrategia ESM en dev. También es el motor que hay debajo.
En Vite 8 (2026), hay un único bundler en Rust: Rolldown.
Rolldown se encarga de todo: de la transformación de módulos en desarrollo y del empaquetado para producción. Está escrito en Rust (a diferencia de JS o Go), lo que le da 10-30 veces más velocidad que los bundlers anteriores. Tiene un único sistema de plugins, compatible con el API que ya conocías de Rollup.
El minificador por defecto también es nuevo: oxc. Un minificador elimina espacios, comentarios y acorta nombres de variables para reducir el tamaño del JS final sin cambiar lo que hace. oxc lo hace en Rust, a una velocidad que los minificadores anteriores en JavaScript no pueden igualar. Por ahora basta saber que es quien comprime y optimiza el JS antes de meterlo en dist/.
Un poco de historia (para entender lo que verás en tutoriales viejos)
Vite no siempre usó un único bundler. Hasta Vite 7, la arquitectura era distinta:
- Desarrollo: usaba esbuild, un transpilador escrito en Go muy rápido, para transformar cada módulo al vuelo.
- Producción: usaba Rollup, un bundler centrado en la calidad del output, para generar el build final.
Rollup es un bundler de JavaScript especializado en producir código limpio y bien optimizado: analiza el grafo de módulos, elimina el código que nadie usa (tree-shaking) y genera chunks lo más pequeños posible. Su sistema de plugins fue tan bien diseñado que se convirtió en el estándar de facto para extender bundlers: Vite, Rolldown y muchas otras herramientas lo adoptaron o lo mantienen compatible. Por eso, si lees documentación antigua de Vite o algún plugin externo, verás referencias a la API de Rollup — esas mismas opciones siguen funcionando en su mayoría.
Dos herramientas distintas significaban posibles diferencias entre lo que veías en dev y lo que salía en prod. Vite 8 lo resuelve con Rolldown: mismo motor para los dos entornos.
Si ves configuración así en un proyecto existente:
// vite.config.js de Vite 7 o anterior — ya está deprecado
export default defineConfig({
build: {
// <-- en Vite 8 se llama rolldownOptions
rollupOptions: {
output: {
manualChunks: { vendor: ['react', 'react-dom'] },
},
},
},
});En Vite 8 el campo correcto es rolldownOptions, no rollupOptions. La lógica es la misma; solo cambia el nombre para reflejar que quien hace el trabajo ahora es Rolldown.
La estructura de un proyecto Vite#
Cuando andamias un proyecto con npm create vite, obtienes esta estructura:
team-builder/
├── index.html ← el ENTRY: Vite parte de aquí, no de un JS suelto
├── vite.config.js ← configuración de Vite (puedes crearlo si no existe)
├── package.json ← manifiesto: scripts y dependencias
├── src/
│ └── main.js ← tu código (desde aquí tiras los imports)
└── public/
└── favicon.ico ← ficheros que se sirven tal cual, sin pasar por Viteindex.html: el entry real#
En Webpack el entry era un fichero JS. En Vite es el index.html. Vite lo lee, encuentra el <script type="module"> y sigue el grafo de imports desde ahí:
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>Team Builder</title>
</head>
<body>
<div id="app"></div>
<!-- El entry: Vite sigue este script y sirve sus imports al navegador -->
<script type="module" src="/src/main.js"></script>
</body>
</html>src/: tu código#
Todo lo que importas desde tus módulos vive aquí. Vite lo procesa cuando el navegador lo pide (en dev) o lo empaqueta todo (en build).
public/: ficheros sin procesar#
Los ficheros de public/ se copian a dist/ tal cual, sin transformaciones. Úsala para:
- El
favicon.icoo iconos PWA - Un
robots.txt - Imágenes que referencias por URL en CSS (no por import en JS)
Si importas una imagen en JS (import logoUrl from './logo.png'), no va en public/: va en src/ y Vite la gestiona, le añade hash para caché y la optimiza.
import.meta: datos del entorno#
Vite expone información del entorno a través de import.meta. El más usado es import.meta.env, que da acceso a las variables de entorno. Lo vemos en el capítulo 4.
// Adelanto: en el capítulo 4 aprenderás a usar import.meta.env
// Por ahora, solo para que sepas que existe:
// "development" o "production"
console.log('Modo: ' + import.meta.env.MODE);vite.config#
El fichero vite.config.js en la raíz del proyecto configura Vite. No es obligatorio: si no existe, Vite usa sus valores por defecto (dev en el puerto 5173, sin alias). Pero en cualquier proyecto real lo querrás.
La forma más común usa defineConfig, un helper de Vite que activa el autocompletado en el editor:
// vite.config.js — configuración del proyecto Team Builder
// helper que activa autocompletado y comprobación de tipos
import { defineConfig } from 'vite';
export default defineConfig({
server: {
// el dev server escuchará en http://localhost:3000
// (por defecto: 5173; fijarlo evita conflictos entre proyectos)
port: 3000,
},
resolve: {
alias: {
// import { heroes } from '@/datos/heroes' apunta a src/datos/heroes
// sin tener que contar ../.. según lo anidado que estés
'@': '/src',
},
},
});Con el alias @ puedes importar así desde cualquier fichero, sin importar en qué carpeta estés:
// Antes, desde src/ui/tabla.js, tenías que escribir:
// dos niveles arriba
import { heroes } from '../../datos/heroes.js';
// Con el alias @, desde cualquier sitio:
// siempre la misma ruta
import { heroes } from '@/datos/heroes.js';Si usas TypeScript, el alias también necesita declararse en tsconfig.json. El capítulo de TypeScript lo cubre.
dev, build, preview#
Un proyecto Vite generado con npm create vite trae estos tres scripts en su package.json:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}npm run dev#
Arranca el servidor de desarrollo. No genera ningún fichero: todo vive en memoria y se sirve bajo demanda. Tiene HMR activo. Es lo que usas mientras programas.
npm run build#
Genera la carpeta dist/ con el proyecto listo para producción. Es Rolldown —el mismo motor en Rust del que hablábamos antes— quien procesa todo el grafo de módulos, resuelve los imports, aplica optimizaciones y deja que oxc minifique el JS. El resultado:
dist/
├── index.html ← HTML con rutas a los assets hasheados
└── assets/
├── index-Ce4KZ7bm.js ← bundle JS minificado (el hash cambia con el contenido)
└── index-BQ3FMZQB.css ← CSS empaquetadoEl hash en el nombre del fichero es intencional: si el contenido cambia, cambia el hash, y el navegador descarga la versión nueva en vez de usar la caché.
npm run preview#
Sirve dist/ con un servidor local. Úsalo antes de desplegar para comprobar que el build funciona igual que el dev server. Es fácil que algo funcione en dev (ESM directo) pero falle en prod (bundle), especialmente con rutas de assets o rutas absolutas mal puestas.
# genera dist/
npm run build
# sirve dist/ en http://localhost:4173 para revisar
npm run previewComprueba lo que sabes#
Pregunta 1 de 5
¿Por qué el dev server de Vite arranca casi al instante?
Tu turno#
Este ejercicio se hace en local: abre el Team Builder que tienes de los capítulos anteriores y practica el ciclo completo con Vite. Cuando termines, despliega las soluciones y compara.
Ejercicio · hazlo en local
Configura y domina tu proyecto Vite
Sobre el Team Builder con Vite que llevas del Nivel 3, practica el ciclo completo: arranca el dev server, comprende la estructura, configura el proyecto con alias y puerto fijo, y si llegas al nivel excelente, añade code splitting para que los módulos pesados no bloqueen la carga inicial.
Paso 1: Que funcione
- Arrancas el proyecto con `npm run dev` y el navegador abre en `localhost:5173`.
- Editas un fichero en `src/` y ves el cambio reflejado en el navegador sin recargar.
- Localizas los tres directorios clave: `index.html` (entry), `src/` y `public/`.
- Generas el build de producción con `npm run build` y compruebas que existe `dist/`.
Paso 2: Que sea predecible
- Creas (o editas) `vite.config.js` con `defineConfig` importado de `vite`.
- Fijas el puerto del dev server en `server.port: 3000`.
- Añades un alias `'@': '/src'` en `resolve.alias` y lo usas en al menos un import.
- Compruebas que `npm run dev` arranca en `localhost:3000` y los imports con `@` funcionan.
Paso 3: Que cargue rápido
- Identificas un módulo del Team Builder que solo se usa bajo demanda (estadísticas, un panel secundario, un módulo de exportación).
- Lo cargas con `import()` dinámico dentro de un handler de evento (click, submit...).
- Generas el build con `npm run build` y en `dist/assets/` aparecen al menos dos chunks JS distintos.
- Puedes explicar por qué el bundle inicial es ahora más pequeño.
Cómo hacerlo en local
Clona el repositorio del curso, entra en la carpeta del ejercicio y abre el
index.html en tu navegador. Toda tu solución va en
solucion.js.
git clone <repo>
cd exercises/nivel-4/vite
# abre index.html en el navegador y edita solucion.js Ver soluciones
# OK — arrancar el Team Builder con Vite y entender su estructura.
# arranca el dev server en http://localhost:5173 (recarga en caliente)
npm run dev
# Estructura clave del proyecto Vite:
# index.html -> el ENTRY: el bundler parte de aquí (no de un main.js suelto)
# busca el <script type="module" src="/src/main.js"> dentro del HTML
# src/ -> tu código fuente (main.js, componentes, módulos)
# public/ -> ficheros que se sirven tal cual, sin pasar por el bundler
# (favicon.ico, robots.txt, imágenes estáticas que no importas)
# genera dist/ con el código optimizado para producción
npm run build
# dist/ tendrá algo como:
# dist/index.html -> el HTML con las rutas a los assets hasheados
# dist/assets/index-abc.js -> el bundle JS minificado (Rolldown + oxc)
# dist/assets/index-xyz.css -> el CSS empaquetado
# sirve dist/ en local para revisar el build antes de desplegar
# útil para comprobar que el build se ve igual que en dev
npm run preview Por qué este nivel
- Arrancas con el dev server y ves la recarga en caliente: el ciclo guardar-y-ver-al-instante. Es la base de trabajar con Vite.
- Entiendes la estructura: index.html como entry, src/ para tu código, public/ para lo que no debe procesarse.
- npm run build genera dist/: ya sabes cómo se ve el resultado antes de desplegar.
// vite.config.js — configuración del proyecto Team Builder.
// helper que da autocompletado y tipos en el editor
import { defineConfig } from 'vite';
export default defineConfig({
server: {
// el dev server escuchará siempre en http://localhost:3000
// (por defecto Vite usa 5173; fijarlo hace el proyecto predecible
// y evita conflictos entre proyectos que corren a la vez)
port: 3000,
},
resolve: {
alias: {
// ahora import x from '@/datos/heroes' apunta a src/datos/heroes
// sin tener que escribir ../../.. según lo anidado que estés
'@': '/src',
},
},
}); Por qué es mejor que el anterior
- Un alias (@ -> src) quita los ../../.. y hace las rutas legibles desde cualquier fichero del proyecto.
- Un puerto fijo hace el proyecto predecible: siempre en localhost:3000, sin importar qué otros proyectos corran.
- defineConfig no es solo sintaxis: activa el autocompletado y la validación de la config en el editor. Centralizar puerto y alias hace el proyecto predecible y portable entre máquinas del equipo sin tocar los imports.
// Excelente — code splitting: cargar un módulo pesado SOLO cuando se necesita.
//
// import() dinámico (lo viste en nivel-3) devuelve una promesa; Rolldown lo parte
// en un chunk aparte, fuera del bundle inicial.
// Resultado: el JS que el usuario descarga al abrir la app es menor.
// Lo puedes confirmar mirando los ficheros de dist/assets/ tras npm run build:
// verás dos chunks en vez de uno.
// Ejemplo aplicado al Team Builder:
// El módulo de estadísticas avanzadas (winrate, racha, comparativas) es pesado.
// No hace falta descargarlo al abrir la app; solo cuando el usuario pulsa "Ver stats".
// el botón "Ver estadísticas"
const boton = document.getElementById('btn-stats');
boton.addEventListener('click', async function () {
// import() dinámico: el módulo se descarga solo cuando se pulsa el botón
// './stats.js' es el módulo pesado
const { calcularStats } = await import('./stats.js');
const heroes = [
{ nombre: 'Ana', partidas: 120, victorias: 78 },
{ nombre: 'Lucio', partidas: 95, victorias: 51 },
{ nombre: 'Mercy', partidas: 200, victorias: 130 },
];
// llama a la función del módulo recién cargado
const resultado = calcularStats(heroes);
console.log('Stats calculadas: ' + JSON.stringify(resultado));
// el módulo solo se descarga una vez; las siguientes pulsaciones usan la caché
}); Por qué es mejor que el anterior
- Code splitting con import() dinámico: lo pesado se descarga solo cuando hace falta, menos JS inicial. El usuario no espera código que quizás nunca usa.
- Lo confirmas mirando los chunks de dist/: en vez de un solo bundle verás varios ficheros, uno por cada import() dinámico.
- Es la técnica real que usan aplicaciones en producción para mantener el Time to Interactive bajo.