Llevas usando Vite desde el Nivel 4. Escribes npm run dev y el servidor arranca en
milisegundos. Escribes npm run build y aparece una carpeta dist/ con ficheros que
tienen nombres raros como index-B3kXp2mN.js. Lo que nunca has visto es lo que ocurre
entre el momento en que pulsas Enter y el momento en que esos ficheros aparecen.
Este capítulo abre la caja. No para que configures Vite desde cero (ya lo haces), sino
para que entiendas qué decisiones toma el bundler por ti y cuándo necesitas cambiar alguna.
Cuando sepas qué es un grafo de módulos, por qué tree-shaking elimina código que no usas,
o cómo un import() dinámico parte el bundle en dos, podrás leer el output de npm run build
con criterio y optimizarlo cuando la app crezca.
El problema que resuelven los bundlers es concreto. El navegador entiende HTML, CSS y
JavaScript. Pero tú escribes TypeScript, JSX y docenas de ficheros que se importan entre sí
con rutas relativas. El navegador no entiende TypeScript ni JSX. Y aunque entiende módulos ES
(el import/export que llevas usando desde el Nivel 3), cargar cien ficheros separados en
producción es lento: cada fichero es una petición de red, y las peticiones en paralelo tienen
un límite. El bundler traduce, combina, elimina lo que sobra y empaqueta en el mínimo número
de ficheros posible.
El grafo de módulos#
El bundler no lee tus ficheros en orden alfabético. Empieza por el punto de entrada
(normalmente main.tsx o index.tsx) y sigue cada import que encuentra, construyendo
un grafo donde cada módulo es un nodo y cada relación de importación es una arista.
main.tsx
└── App.tsx
├── HeroCard.tsx
│ └── datos/heroes.ts
└── HeroFilter.tsx
└── datos/heroes.tsEn este grafo, datos/heroes.ts aparece dos veces como destino: tanto HeroCard.tsx
como HeroFilter.tsx lo importan. El bundler lo detecta y lo incluye una sola vez en el
bundle final, aunque dos módulos lo necesiten. Sin ese grafo, podría incluir el mismo
fichero dos veces y enviar código duplicado al navegador.
El grafo también es la base del tree-shaking: si un módulo existe en el proyecto pero nadie lo importa, no forma parte del grafo y no entra en el bundle. Si un módulo tiene cinco exports pero solo se importa uno, el bundler solo incluye ese uno.
Transpilación: traducir lo que el navegador no entiende#
El navegador no entiende TypeScript ni JSX. Antes de empaquetar, el bundler tiene que transpilar: convertir el código que escribiste en JavaScript que el navegador ejecuta.
La transpilación de JSX es sencilla de ver. Este componente:
// el componente que escribe el desarrollador
function HeroCard({ nombre }: { nombre: string }) {
// el JSX se parece a HTML pero es una sintaxis especial de JavaScript
return <li className="hero-card">{nombre}</li>;
}Se convierte en una llamada a función que el navegador entiende:
// el resultado tras transpilar: sin JSX, solo JavaScript
function HeroCard({ nombre }) {
return React.createElement("li", { className: "hero-card" }, nombre);
}Ese React.createElement es el modelo mental clásico. Con el runtime JSX automático
—el que usan hoy Vite y @vitejs/plugin-react por defecto— el JSX se transpila a
_jsx(...) de react/jsx-runtime y ya no hace falta importar React en cada fichero.
El resultado es el mismo: una llamada a función que el navegador entiende.
La transpilación de TypeScript elimina los tipos. El navegador no puede ejecutar anotaciones de tipo porque no son parte de JavaScript: el transpilador las borra y deja solo el código de lógica. El comportamiento del programa no cambia; solo desaparece la capa de tipos que TypeScript usaba para comprobar en tiempo de build.
La transpilación es distinta de minificar. Transpilar cambia el lenguaje o la sintaxis. Minificar toma JavaScript válido y lo hace más pequeño sin cambiar lo que hace. Son dos pasos del build que ocurren uno después del otro.
Tree-shaking: tirar el código muerto#
Si tienes un módulo de utilidades con tres funciones pero solo usas una en tu aplicación, ¿deberían entrar las tres en el bundle? Sin tree-shaking, sí. Con tree-shaking, no.
// utils.ts — tres funciones exportadas
export function calcularRatio(victorias: number, partidas: number) {
return victorias / partidas;
}
export function formatearRol(rol: string) {
return rol.toUpperCase();
}
export function contarTanques(heroes: { rol: string }[]) {
return heroes.filter((h) => h.rol === "tanque").length;
}// App.tsx — solo importa una
import { calcularRatio } from "./utils";El bundler analiza el grafo y ve que formatearRol y contarTanques no se importan
en ningún fichero. Las marca como código muerto y no las incluye en el bundle. El usuario
no descarga ni parsea esas funciones.
El tree-shaking funciona porque los módulos ES son estáticos: el bundler sabe en
tiempo de build qué se exporta y qué se importa, sin ejecutar el código. Si usaras el
formato CommonJS (require/module.exports), el bundler no podría hacer ese análisis
porque los require pueden ser dinámicos (dentro de un if, con una variable como ruta)
y el bundler no puede saber en build qué se va a importar en runtime.
El “¿y qué?” de no tener tree-shaking: código muerto que viaja por la red, ocupa espacio en caché y tarda en parsear. En una librería grande donde solo necesitas dos funciones, sin tree-shaking pagas el coste de todo el módulo.
Minificación: el mismo código, más pequeño#
Tras transpilar y eliminar código muerto, el bundler minifica: toma el JavaScript resultante y lo comprime al mínimo sin cambiar lo que hace.
El proceso elimina lo que el intérprete no necesita:
- Espacios en blanco, saltos de línea y comentarios
- Nombres de variables locales, que se acortan a letras de una sola letra
// antes de minificar — legible para el desarrollador
function calcularRatio(victorias, partidas) {
// victorias dividido entre partidas, redondeado a dos decimales
return Math.round((victorias / partidas) * 100) / 100;
}// después de minificar — idéntico en comportamiento, más pequeño en bytes
function c(a,b){return Math.round(a/b*100)/100}La función hace exactamente lo mismo. Los nombres a y b son válidos JavaScript.
El comentario no existía para el intérprete. El resultado ocupa una fracción del espacio.
La minificación es distinta de transpilar. Minificar no cambia el lenguaje: el resultado sigue siendo JavaScript. Transpilar cambia la sintaxis o el lenguaje. En el pipeline de Vite 8, Rolldown transpila TypeScript y JSX a JavaScript y luego minifica el resultado en un solo paso unificado. Antes de Vite 8, esas dos tareas las hacían herramientas distintas (esbuild para la transpilación en desarrollo, Rollup para el bundle de producción), lo que explica por qué verás esa separación en tutoriales y configuraciones anteriores.
Code-splitting: partir el bundle en chunks#
Un bundle único que contiene toda la aplicación tiene un problema: el usuario que solo visita la página de inicio descarga el código de todas las demás páginas. Si el Team Builder tiene una vista de lista, una vista de detalle de héroe, y una vista de estadísticas, el usuario que solo ve la lista paga el coste de descargar las otras dos.
El code-splitting parte el bundle en chunks: ficheros JS independientes que el
navegador descarga bajo demanda. Vite genera un chunk separado cada vez que encuentra
un import() dinámico, que ya viste en el Nivel 3.
En React, el patrón habitual es React.lazy, que ya viste en el Nivel 8:
// en lugar de importar HeroDetail de forma estática al cargar la app,
// lo declaramos como componente perezoso
import { lazy, Suspense } from "react";
// React.lazy recibe una función que devuelve un import() dinámico.
// Vite detecta ese import() en build y genera un chunk separado para HeroDetail.
const HeroDetail = lazy(() => import("./HeroDetail"));
export default function App() {
return (
// Suspense muestra el fallback mientras el chunk viaja por la red.
// Cuando llega, React sustituye el fallback por el componente real.
<Suspense fallback={<p>Cargando detalle del héroe...</p>}>
<HeroDetail />
</Suspense>
);
}Tras ejecutar npm run build con esta configuración, dist/assets/ contiene al menos
dos ficheros JS: el chunk inicial (lo que carga siempre) y el chunk de HeroDetail (lo
que se descarga solo cuando el usuario llega a esa vista). El panel Network de DevTools
muestra ese segundo chunk apareciendo como petición de red en el momento exacto en que
el usuario lo necesita.
El “¿y qué?” de no usar code-splitting: la primera carga de la app es proporcional a todo el código que contiene, no a lo que el usuario va a ver. En una aplicación con muchas vistas, eso puede marcar la diferencia entre una experiencia que arranca rápido y una que hace esperar.
Source maps: depurar el código de producción#
El bundle minificado de producción tiene esta pinta:
function c(a,b){return Math.round(a/b*100)/100}var d=["Reinhardt","Ana","Lucio"]Si algo falla en producción y el stack trace dice index-B3kXp2mN.js:1:18432, no
sabes qué fichero de tu código corresponde a esa posición. Los source maps resuelven
ese problema.
Un source map es un fichero .map que el bundler genera junto al bundle minificado.
Contiene una tabla que mapea cada posición del bundle de vuelta a la posición original
en tu código fuente: index-B3kXp2mN.js:1:18432 corresponde a src/utils.ts:47:5.
Los DevTools del navegador descargan el .map automáticamente cuando están abiertos
y el usuario activa la pestaña Sources. Con el source map cargado, el error aparece
en tu TypeScript original con los nombres de variables y funciones que escribiste.
Lo que no pasa: el usuario que navega sin abrir DevTools no descarga el .map. El
navegador solo lo pide cuando los DevTools están activos y necesita mostrarte el código
fuente. Activar source maps en producción tiene un coste de almacenamiento (los .map
son grandes) pero coste cero para el usuario final.
Para activarlos en Vite, una línea en vite.config.ts:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
build: {
// genera un fichero .map junto a cada chunk JS de producción
sourcemap: true,
},
});El zoo de bundlers, y cuándo cada uno#
En 2026 hay más de un bundler en uso activo. No todos hacen lo mismo ni encajan en los mismos contextos.
| Bundler | Motor | Cuándo usarlo |
|---|---|---|
| Vite + Rolldown | Rust (Rolldown), dev y prod | Proyecto nuevo. El estándar de facto en 2026 |
| webpack | JavaScript | Proyecto heredado con configuración compleja o plugins sin alternativa en Vite |
| rspack | Rust, compatible webpack | Proyecto heredado grande que necesita acelerar su build sin reescribir la config |
| esbuild | Go | Herramienta de transformación rápida o bundler simple; ecosistema de plugins limitado |
| Rollup | JavaScript | Librerías: genera ESM limpio con tree-shaking fino y formatos múltiples |
| Turbopack | Rust, integrado en Next.js | Si ya usas Next.js de Vercel |
Vite con Rolldown es la elección para proyectos nuevos. Desde Vite 8, Rolldown (escrito en Rust) es el bundler unificado para desarrollo y producción: arranque en milisegundos y HMR sin recargar la página completa en dev, build optimizado en prod, todo con el mismo motor. Rolldown reemplaza al dúo anterior —esbuild para la transformación en desarrollo y Rollup para el bundle de producción— que verás citado en tutoriales y configuraciones anteriores a Vite 8. La capa de transformación y minificación de Rolldown se apoya en Oxc (un toolchain en Rust), no en esbuild.
webpack no está muerto. Miles de proyectos en producción llevan una configuración de webpack construida durante años con plugins, loaders y ajustes que no tienen equivalente directo en Vite. Migrar esa configuración tiene un coste que a menudo no se justifica. Para esos proyectos, rspack es una alternativa interesante: es compatible con la API de webpack (mismos conceptos, misma estructura de configuración) pero escrito en Rust, lo que acelera los builds de forma significativa sin reescribir la configuración desde cero.
Turbopack tiene sentido solo si ya usas Next.js. Fue Next.js 16 quien lo hizo estable
y lo activó por defecto tanto en next dev como en next build; en la versión 15 era
opt-in. Si no usas Next.js, no hay razón para elegirlo.
Los ficheros que orquestan el build#
Con Vite, toda la configuración de build vive en un solo fichero: vite.config.ts en la
raíz del proyecto. Este es un ejemplo con las opciones más habituales para producción:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
// plugins: extensiones que añaden capacidades al pipeline de Vite.
// @vitejs/plugin-react habilita JSX, Fast Refresh en desarrollo,
// y el soporte de React en el servidor de desarrollo.
plugins: [react()],
build: {
// sourcemap: genera ficheros .map para depurar producción
sourcemap: true,
// rolldownOptions configura el bundler de Vite 8 (Rolldown).
rolldownOptions: {
output: {
// codeSplitting agrupa los módulos en chunks con reglas declarativas.
codeSplitting: {
// cada grupo da nombre a un chunk y define qué módulos entran en él.
groups: [
// todo lo que venga de node_modules (React, react-dom y demás
// dependencias) va a un chunk "vendor" con su propio hash, estable
// entre deploys mientras no actualices dependencias.
{ name: "vendor", test: /node_modules/ },
],
},
},
},
},
});Ese codeSplitting es la opción nativa de Rolldown, la forma actual en Vite 8. La que
verás en la mayoría de configs y tutoriales todavía es la de la API de Rollup,
rollupOptions.output.manualChunks: una función que recibe el id de cada módulo y devuelve el
nombre de su chunk ((id) => id.includes('node_modules') ? 'vendor' : undefined). Rolldown la
mantiene por compatibilidad, pero está deprecada; el concepto —agrupar las dependencias en
su propio chunk— es idéntico. En un proyecto nuevo usa codeSplitting; cuando abras un
vite.config.ts heredado, reconocerás manualChunks.
Para comparar, este es el equivalente de webpack: la misma idea (entry, output, loaders para TypeScript y JSX, plugins) pero con mucha más configuración explícita:
// webpack.config.js — lo mismo que el vite.config.ts de arriba, pero manual
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// punto de entrada de la aplicación
entry: "./src/main.tsx",
output: {
// carpeta de salida y nombre del bundle con hash de contenido
path: path.resolve(__dirname, "dist"),
filename: "[name].[contenthash].js",
clean: true,
},
module: {
rules: [
{
// loader que transpila TypeScript y JSX a JavaScript
test: /\.(ts|tsx)$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
optimization: {
splitChunks: {
cacheGroups: {
// separa node_modules en un chunk vendor, igual que manualChunks de Vite
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendor",
chunks: "all",
},
},
},
},
plugins: [
// genera el index.html con las referencias a los chunks automáticamente
new HtmlWebpackPlugin({ template: "./index.html" }),
],
resolve: {
// permite importar ficheros .ts y .tsx sin escribir la extensión
extensions: [".ts", ".tsx", ".js"],
},
};La diferencia de complejidad es real. Con Vite, los loaders para TypeScript y JSX, la generación del HTML con los assets hasheados, y el source map están activos por defecto o con una línea de configuración. Con webpack, cada pieza se configura de forma explícita. Esa explicitud es útil cuando necesitas control fino; es innecesaria cuando el comportamiento por defecto es el que quieres.
Comprueba lo que sabes#
Pregunta 1 de 7
¿Cuál es la diferencia entre transpilar y minificar?
Tu turno#
El ejercicio se hace en local, sobre el Team Builder de React que vienes construyendo desde el Nivel 7. No hay playground en esta página: mirar el output de un bundler de verdad requiere un proyecto de verdad, con dependencias reales y un proceso de build que genere ficheros que puedas inspeccionar en tu sistema de ficheros. Los ficheros de solución están para que los leas después: la comparación entre lo que hiciste y la solución es donde está el aprendizaje.
Ejercicio · hazlo en local
Inspeccionar y optimizar el bundle del Team Builder
Sobre el Team Builder de React que vienes construyendo desde el Nivel 7, construye el bundle de producción con Vite y lee lo que hay dentro. Después aplica code-splitting con React.lazy y configura Vite para separar las dependencias en un chunk estable. Las soluciones muestran cada tier con su explicación. Haz el tuyo primero y luego léelas: la comparación es donde está el aprendizaje.
Paso 1: Construir e inspeccionar el bundle
- Ejecutas `npm run build` y el proceso termina sin errores.
- Listas `dist/assets/` e identificas al menos un chunk JS y un fichero CSS, ambos con hash en el nombre.
- Abres `dist/index.html` y localizas las etiquetas `<script>` y `<link>` que referencian esos assets con el nombre exacto con hash.
- Puedes explicar por qué los nombres llevan hash y qué relación tiene con la caché del navegador.
- Compruebas que `dist/assets/*.js | wc -l` devuelve 1: todo el código de la app va en un único chunk inicial.
Paso 2: Code-splitting con React.lazy
- Identificas una vista o componente pesado del Team Builder (por ejemplo, el detalle de un héroe) que no se muestra en la carga inicial.
- Conviertes esa importación estática en `React.lazy(() => import("./HeroDetail"))` y envuelves el uso con `<Suspense fallback={...}>`.
- Tras reconstruir con `npm run build`, `dist/assets/` contiene al menos dos ficheros JS: el chunk inicial y el chunk del componente lazy.
- Verificas en el panel Network de DevTools que el segundo chunk solo aparece como petición de red cuando el usuario navega a esa vista, no en la carga inicial.
Paso 3: Chunk de vendor estable y source maps
- Añades `manualChunks` en `vite.config.ts` para que todo lo de `node_modules` vaya a un chunk llamado `vendor`.
- Tras reconstruir, `dist/assets/` muestra al menos tres ficheros JS: el vendor, el chunk inicial de tu app, y el chunk lazy del tier anterior.
- Activas `sourcemap: true` en la sección `build` de `vite.config.ts`.
- Compruebas que en `dist/assets/` aparecen ficheros `.map` junto a cada chunk JS.
- Puedes explicar cuándo descarga el navegador los `.map` y por qué el chunk de vendor tiene una vida útil en caché mucho mayor que el chunk de tu aplicación.
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-9/bundling-a-fondo
# abre index.html en el navegador y edita solucion.js Ver soluciones
#!/usr/bin/env bash
# Tier ok: construir el bundle e inspeccionar el output.
# Demuestra qué genera Vite al hacer un build de producción: ficheros JS y CSS
# con hash en el nombre, un HTML que los referencia, y un único chunk inicial
# que contiene toda la lógica de la aplicación.
# --- 1. Construir el bundle de producción ---
# vite build lee tu vite.config.ts, transpila TypeScript, aplica tree-shaking,
# minifica el resultado y escribe todo en dist/
npm run build
# --- 2. Ver la estructura completa de dist/ ---
# lista recursivamente todos los ficheros generados en el directorio de salida
ls -R dist
# lo que verás tiene esta forma (los hashes cambian en cada build):
#
# dist/:
# assets/ index.html
#
# dist/assets:
# index-B3kXp2mN.js <- chunk inicial: toda tu app en un solo fichero
# index-CqR7vWlA.css <- todos tus estilos en un solo fichero
# --- 3. Ver los tamaños reales de cada fichero ---
# du (disk usage) con -h (human readable) muestra kilobytes y megabytes legibles
# el asterisco expande todos los ficheros dentro de assets/
du -h dist/assets/*
# lo que verás:
# 142K dist/assets/index-B3kXp2mN.js <- JS minificado: React + tu app juntos
# 8K dist/assets/index-CqR7vWlA.css
# --- 4. Mirar cómo index.html referencia los assets ---
# cat vuelca el contenido del fichero en el terminal
cat dist/index.html
# lo que verás en el <head>:
# <link rel="stylesheet" crossorigin href="/assets/index-CqR7vWlA.css">
#
# y al final del <body>:
# <script type="module" crossorigin src="/assets/index-B3kXp2mN.js"></script>
#
# Vite inyecta los nombres exactos con hash en el HTML automáticamente.
# No necesitas actualizarlos a mano: el HTML siempre apunta al fichero correcto.
# --- 5. Entender por qué los nombres llevan hash ---
# el hash (B3kXp2mN) es una huella del CONTENIDO del fichero.
# si el contenido no cambia entre dos builds, el hash no cambia.
# eso significa que el navegador puede guardar el fichero en caché con una
# cabecera "Cache-Control: max-age=31536000" (un año) sin miedo a servir
# una versión vieja: si el código cambia, el hash cambia, el nombre cambia,
# y el navegador descarga el fichero nuevo.
# si el código no cambia (por ejemplo, la librería de React), el hash es el
# mismo y el navegador sirve el fichero desde su caché local sin hacer ninguna
# petición de red.
# cuentas cuántos ficheros JS hay en assets/ para comprobar que solo hay uno
ls dist/assets/*.js | wc -l
# devuelve: 1
# todo el código (React, react-dom, tus componentes, tus hooks, los datos de
# héroes) va empaquetado en ese único fichero. el navegador tiene que descargarlo
# entero antes de que la app pueda arrancar, aunque el usuario solo vaya a ver
# la página de inicio y nunca abra el detalle de un héroe.
# --- Límite de este tier ---
# Inspeccionar el output es el primer paso, pero no cambia nada del bundle.
# El problema de este tier es que hay un solo chunk JS: si la app crece
# (más componentes, más vistas, más lógica), ese fichero único se hace cada
# vez más grande y el tiempo de carga inicial crece con él. El usuario paga
# el coste de descargar código que quizá no necesita en esa visita.
# El tier "mejor" resuelve esto partiendo el bundle en trozos que el navegador
# descarga solo cuando los necesita. Por qué este nivel
- El primer paso siempre es mirar lo que genera el bundler antes de intentar optimizarlo. Este tier enseña a leer el output de Vite: los nombres con hash, la estructura de `dist/`, y cómo `index.html` referencia los assets generados.
- Su límite: todo el código (React, react-dom, tus componentes, tus datos de héroes) va en un único chunk JS. Cuanto más crece la app, más tarda la primera carga. El usuario que solo visita la lista paga el coste de descargar el código del detalle que quizá nunca abre. El tier "mejor" parte ese bundle en trozos más pequeños.
// Tier mejor: code-splitting con React.lazy y Suspense.
// Demuestra cómo partir el bundle en chunks que el navegador descarga bajo
// demanda: el chunk del detalle del héroe no se incluye en la carga inicial,
// solo se descarga cuando el usuario navega a esa vista por primera vez.
import { lazy, Suspense, useState } from "react";
import { heroes } from "./data/heroes";
// React.lazy recibe una función que llama a import() dinámico.
// Vite detecta ese import() en tiempo de build y genera un chunk separado
// para HeroDetail: un fichero JS propio en dist/assets/ con su propio hash.
// El navegador NO descarga ese fichero hasta que React lo necesita de verdad.
const HeroDetail = lazy(() => import("./HeroDetail"));
// el tipo del héroe tal como viene de tu lista de datos
type Hero = {
id: number;
nombre: string;
rol: "tanque" | "dano" | "soporte";
partidas: number;
victorias: number;
};
export default function App() {
// heroSeleccionado guarda el héroe que el usuario ha elegido ver en detalle,
// o null si está en la vista de lista
const [heroSeleccionado, setHeroSeleccionado] = useState<Hero | null>(null);
return (
<main>
<h1>Team Builder</h1>
{heroSeleccionado === null ? (
// --- Vista de lista ---
// Esta parte va en el chunk inicial: se descarga siempre.
<ul>
{heroes.map((heroe: Hero) => (
<li key={heroe.id}>
<span>{heroe.nombre}</span>
<button
// al hacer clic se guarda el héroe en el estado,
// lo que provoca que React intente renderizar HeroDetail
// y en ese momento el navegador descarga su chunk
onClick={() => setHeroSeleccionado(heroe)}
>
Ver detalle
</button>
</li>
))}
</ul>
) : (
// --- Vista de detalle ---
// Suspense es el mecanismo de React para esperar a que un componente
// perezoso termine de cargarse. Mientras el chunk de HeroDetail se
// descarga de la red, React muestra el fallback en su lugar.
// Cuando el chunk llega, React sustituye el fallback por el componente.
<Suspense fallback={<p>Cargando detalle...</p>}>
{/* HeroDetail solo se renderiza (y descarga) cuando llegamos aquí */}
<HeroDetail
heroe={heroSeleccionado}
// volver a null devuelve al usuario a la lista
onVolver={() => setHeroSeleccionado(null)}
/>
</Suspense>
)}
</main>
);
}
// --- Por qué este tier supera al anterior ---
// El tier "ok" generaba un único chunk JS con todo el código dentro.
// Aquí, el chunk de HeroDetail es independiente: el usuario que solo visita
// la lista nunca lo descarga. Cuanto más grande y complejo sea el detalle
// (estadísticas, gráficas, animaciones), más peso se quita de la carga inicial.
// La diferencia se ve en el panel Network de DevTools: tras el primer build
// con lazy, dist/assets/ tiene dos ficheros JS en lugar de uno. El segundo
// solo aparece en las peticiones de red cuando el usuario pulsa "Ver detalle".
// El límite de este tier es que todos los chunks de tu propio código cambian
// con cada build (cualquier cambio en App.tsx regenera su hash y el navegador
// lo descarga de nuevo), pero el código de React y react-dom también va
// mezclado en ese chunk y también se invalida aunque no hayas tocado
// las dependencias. El tier "excelente" separa vendor de aplicación para que
// cada uno tenga su propio ciclo de caché. Por qué es mejor que el anterior
- Con `React.lazy` y un `import()` dinámico, Vite genera un chunk separado para `HeroDetail`. El navegador no lo descarga en la carga inicial: solo lo pide cuando el usuario pulsa "Ver detalle". El panel Network de DevTools muestra ese segundo chunk apareciendo justo en ese momento.
- Su límite: aunque el bundle ahora está partido, el código de React y react-dom sigue mezclado con el código de tu aplicación en el mismo chunk. Cualquier cambio en un componente regenera el hash de ese chunk y el navegador descarga React de nuevo aunque no hayas tocado las dependencias. El tier "excelente" separa el vendor para que cada parte tenga su propio ciclo de caché.
// Tier excelente: chunks de vendor separados y source maps de producción.
// Demuestra cómo controlar la estrategia de partición del bundle para que el
// código de las dependencias (React, react-dom) viva en un chunk propio que
// se cachea de forma estable, independiente del código de tu aplicación.
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [
// el plugin oficial de React para Vite: habilita JSX, Fast Refresh en dev,
// y el soporte de React en el servidor de desarrollo
react(),
],
build: {
// sourcemap: true le dice a Vite que genere un fichero .map junto a cada
// chunk JS. Ese fichero mapea cada línea del JS minificado de vuelta a la
// línea original de tu TypeScript o JSX antes de la transformación.
// Sin source map, el stack trace de un error en producción apunta a una
// línea ilegible del bundle; con él, los DevTools del navegador muestran
// tu código fuente original, con nombres de variables y funciones legibles.
// El .map no lo descarga el usuario común: el navegador solo lo pide cuando
// los DevTools están abiertos y el usuario activa la pestaña Sources.
sourcemap: true,
// rolldownOptions configura el bundler de Vite 8 (Rolldown).
rolldownOptions: {
output: {
// codeSplitting es la opción NATIVA de Rolldown para controlar cómo se
// agrupan los módulos en chunks. Es declarativa: defines grupos, cada uno
// con un nombre de chunk y una regla de qué módulos entran en él.
codeSplitting: {
groups: [
// todo módulo cuya ruta (id) case con /node_modules/ va al chunk
// "vendor": React, react-dom y demás dependencias acaban en
// vendor-<hash>.js, con un hash estable que no se invalida cuando
// cambias tu propio código de aplicación.
{ name: "vendor", test: /node_modules/ },
],
},
// Lo que no cae en ningún grupo (tu código: componentes, hooks, datos)
// lo reparte Rolldown con su lógica por defecto, que respeta los
// import() dinámicos del tier "mejor".
// (En configs heredadas verás en su lugar la API de Rollup:
// rollupOptions.output.manualChunks, una función (id) => "vendor" | undefined
// que Rolldown mantiene por compatibilidad pero que está deprecada en Vite 8.)
},
},
},
});
// --- Por qué este tier supera al anterior ---
// El tier "mejor" introdujo code-splitting dinámico, pero todos los chunks
// siguen mezclando tu código con el de las dependencias. React y react-dom
// pesan unos 140 KB minificados y gzipeados: son los mismos en cada build.
// Sin manualChunks, cualquier cambio en un componente tuyo regenera el hash
// del chunk que lo contiene, aunque React no haya cambiado, y el navegador
// descarga React de nuevo junto con tu cambio.
// Con manualChunks, React y react-dom van en vendor-<hash>.js. Ese hash solo
// cambia cuando actualizas las dependencias (npm install <nueva-versión>).
// El resto del tiempo, el usuario ya tiene vendor en caché y solo descarga
// el chunk de tu aplicación, que es mucho más pequeño.
// El source map completa el cuadro: en producción, si un usuario reporta un
// error con un stack trace, puedes abrirlo en los DevTools de tu máquina,
// activar source maps y ver exactamente la línea de tu TSX original que lo
// provocó, sin tener que descifrar el bundle minificado. Por qué es mejor que el anterior
- Con `manualChunks`, React y react-dom van a `vendor-<hash>.js`. Ese hash solo cambia cuando actualizas las dependencias. El resto del tiempo, el usuario tiene el vendor en caché y solo descarga el chunk de tu aplicación, que es mucho más pequeño. Los source maps completan el cuadro: si algo rompe en producción, los DevTools muestran tu TypeScript original en lugar del bundle minificado.
- Esta configuración es el punto de partida estándar para cualquier app Vite que va a producción. No es sobreingeniería: es lo mínimo para que la caché del navegador trabaje a tu favor y para que los errores de producción sean depurables sin adivinar en el bundle minificado.