wandres.dev
OPTIMIZEDEPS · pre-bundling

Por qué existe el pre-bundling

El dev server de Vite sirve ESM nativo, pero el navegador ni entiende CommonJS ni tolera cientos de peticiones por una sola dependencia. El pre-bundling resuelve ambas cosas: convierte CJS a ESM y agrupa las deps con muchos módulos internos en un único artefacto. Por qué sin este paso el arranque en dev sería inviable.

⏱ 13 min

La promesa del dev server de Vite es seductora: servir tu código como ESM nativo, sin empaquetar nada, para que el navegador arranque al instante. Pero esa promesa choca contra dos rocas que viven dentro de node_modules: dependencias publicadas en CommonJS que el navegador no sabe importar, y dependencias estalladas en cientos de módulos internos que ahogarían al navegador en peticiones. El pre-bundling es la excepción quirúrgica que salva la promesa: empaqueta solo las dependencias —nunca tu código— para que el “sin bundling en dev” siga siendo cierto justo donde importa.

🎯 Al terminar esta lección sabrás
  • Entender por qué el dev server no empaqueta tu código pero sí tus dependencias.
  • Ver los dos problemas que el navegador no resuelve solo: CommonJS y la cascada de peticiones.
  • Comprender cómo agrupar módulos internos convierte cientos de peticiones en una.
  • Situar optimizeDeps como la excepción deliberada al principio de “sin bundling en dev”.

El dev server sirve ESM nativo, hasta que llega a node_modules

El modelo mental de Vite en desarrollo es que el navegador es quien monta el grafo. Tú escribes import y export, Vite transforma cada archivo bajo demanda —TypeScript a JavaScript, JSX a llamadas— y lo entrega tal cual, un módulo por petición. No hay un bundle inicial que construir, y por eso el servidor arranca en milisegundos por muy grande que sea el proyecto: solo se transforma lo que el navegador pide al navegar.

Ese modelo funciona de maravilla para tu código, que tú controlas y escribes en ESM moderno. Pero hay una frontera nítida. En cuanto un archivo importa un paquete como lodash-es, ese especificador bare —el nombre pelado, sin ./ ni ruta relativa— apunta a otro mundo: node_modules, poblado por miles de paquetes que no escribiste tú y que no siguen tus reglas.

// Dos clases de import muy distintas para el dev server
import { Boton } from "./ui/boton";    // relativo: TU codigo, se sirve crudo
import { debounce } from "lodash-es";  // bare: una DEP, hay que resolverla

El navegador incluso tiene un mecanismo estándar para resolver nombres pelados, los import maps, pero no basta para node_modules: habría que mapear a mano cada paquete y, con él, cada uno de sus miles de módulos internos, y aun así seguirían en pie los dos problemas de fondo. Vite elige otro camino, más barato y más robusto: interceptar esos especificadores y resolverlos él mismo hacia dependencias ya tratadas.

Porque ahí, en el cruce hacia node_modules, el ESM nativo se rompe por dos motivos independientes. Uno es de compatibilidad de formato y el otro es de rendimiento de red, y el pre-bundling existe precisamente para reparar los dos de una sola pasada, antes de que el navegador pida nada.

Problema 1: el navegador no entiende CommonJS

Buena parte del ecosistema todavía se publica en CommonJS o UMD: paquetes que usan module.exports y require en vez de import y export. El cargador de módulos del navegador no sabe leer eso. La función require sencillamente no existe en el navegador, y module.exports no es sintaxis que su parser entienda.

// Una dependencia publicada en CommonJS: el navegador se atraganta con esto
const react = require("react");
module.exports = function useAlgo() { /* ... */ };

Si Vite sirviera esos archivos crudos, el navegador lanzaría un error de sintaxis al primer require. El pre-bundling los transforma a ESM válido antes de que lleguen: una dependencia CJS entra por un lado y sale hablando el idioma del navegador, con export de verdad, de modo que tu import nativo la consume sin enterarse de que el original no era ESM.

El formato UMD complica un poco más el cuadro, porque un mismo archivo detecta en tiempo de ejecución si vive en un entorno CJS, AMD o global, y se comporta distinto en cada uno. El bundler que hace el pre-bundling analiza el paquete, decide qué rama es la relevante y emite un ESM limpio, sin esa maquinaria de detección que en el navegador solo sería lastre.

De paso, el pre-bundling unifica formatos. Da igual si una dependencia se publicó en CJS, en UMD o en ESM: todas salen del proceso como ESM homogéneo. Esa homogeneidad es en sí misma un valor, porque elimina de raíz toda una clase de errores de interoperabilidad que, de otro modo, aparecerían dispersos y difíciles de diagnosticar en tiempo de ejecución.

Problema 2: la cascada de peticiones

El segundo problema es más sutil y es de rendimiento puro. Muchas librerías ESM modernas se publican fragmentadas en cientos de archivos diminutos, buscando el tree-shaking perfecto: lodash-es, por ejemplo, tiene un módulo por función y un index que los reexporta todos. Es una virtud para el empaquetado de producción, pero una patología para el dev server.

En ESM nativo, importar lodash-es haría que el navegador pidiera el index, descubriera seiscientas reexportaciones y disparara seiscientas peticiones más, cada una con su ida y vuelta de red. Aun sobre HTTP/2, esa cascada de round-trips convierte un arranque en frío en una espera perceptible: el navegador pasa los primeros segundos haciendo poco más que pedir archivos minúsculos uno tras otro.

flowchart TD
subgraph Sin pre-bundling
  A[Navegador pide lodash-es] --> B[index reexporta 600 modulos]
  B --> C[600 peticiones en cascada]
end
subgraph Con pre-bundling
  D[Navegador pide lodash-es] --> E[un unico modulo ya empaquetado]
  E --> F[1 sola peticion]
end
style C fill:#f38ba8,color:#11111b
style F fill:#a6e3a1,color:#11111b

El pre-bundling aplana esa constelación de módulos en un único archivo. El bundler recorre el grafo interno de lodash-es, resuelve todas sus reexportaciones y emite un solo módulo ESM con lo que de verdad usas. Seiscientas peticiones se convierten en una. Multiplícalo por cada dependencia “explotada” de tu árbol y entiendes por qué, sin este paso, el bucle de recarga en frío sería sencillamente inviable.

Conviene desmontar el consuelo fácil de que HTTP/2 lo arregla. El multiplexado reduce el coste de abrir conexiones, pero no elimina el coste por petición: cada módulo sigue teniendo su propia cabecera, su propia resolución y su propio lugar en la cadena de dependencias, que el navegador no puede pedir hasta haber leído el módulo que lo importa. Esa serialización inherente al grafo es la que el pre-bundling colapsa al fundir todo en un artefacto.

El coste histórico era aún peor. Bajo HTTP/1.1, los navegadores limitaban a seis las conexiones simultáneas a un mismo origen, así que seiscientos módulos no se pedían en paralelo sino en tandas de seis, serializando la cascada hasta lo grotesco. HTTP/2 alivió ese cuello de botella concreto, pero, como acabamos de ver, no eliminó el coste por módulo ni la serialización que impone el propio grafo. El pre-bundling ataca la causa —hay demasiados módulos— y no el síntoma de cómo se transportan.

Aspecto Sin pre-bundling Con pre-bundling
Dep en CommonJS error: require no existe en el navegador ESM válido, importable
Dep de 600 módulos 600 peticiones en cascada 1 petición a un artefacto
Arranque en frío tormenta de round-trips una tanda de bundles cacheados

optimizeDeps: la excepción que sostiene la regla

El conjunto de ajustes que gobierna todo esto se llama optimizeDeps, y su nombre engaña un poco. No “optimiza” en el sentido de minificar para producción, sino que reconcilia el mundo heterogéneo de las dependencias con el modelo ESM-nativo del dev server. Es un paso que ocurre una sola vez, al arrancar, antes de que el navegador pida nada.

🔄

Convierte formatos

CommonJS y UMD entran; ESM válido sale. El navegador nunca ve un require ni un module.exports.

🎯

Aplana módulos

Cientos de archivos internos de una dep se funden en uno. La cascada de peticiones desaparece.

🧊

Cachea lo estable

El resultado se guarda en node_modules/.vite; mientras las deps no cambien, no se recalcula.

La asimetría entre tu código y tus dependencias es deliberada, y es la clave de todo. Tu código fuente cambia a cada guardado, así que empaquetarlo mataría el HMR instantáneo; se sirve sin tocar. Tus dependencias, en cambio, casi nunca cambian entre arranques, así que empaquetarlas una vez y cachear el resultado no cuesta casi nada y se amortiza en cada recarga.

ℹ️
Por eso solo se empaquetan las dependencias

Vite empaqueta lo estable y sirve crudo lo volátil, y ese reparto es lo que le permite ser a la vez rápido de arrancar y rápido de iterar. El pre-bundling actúa sobre lo que vive en node_modules, nunca sobre tu src. Son dos regímenes distintos conviviendo en el mismo servidor, y saber cuál gobierna cada archivo es el primer paso para entender —y depurar— cualquier cosa que ocurra en optimizeDeps.

El pre-bundling vive solo en desarrollo

Conviene cerrar con una aclaración que evita un malentendido frecuente: el pre-bundling es un mecanismo exclusivo del dev server. En producción no existe como tal, porque allí Vite ejecuta el bundler completo sobre todo tu grafo —tu código y tus dependencias juntos— con tree-shaking, minificación y code splitting. Las dependencias no necesitan un tratamiento aparte porque ya pasan por el empaquetado real.

# Desarrollo: dev server + pre-bundling de deps en node_modules/.vite
vite

# Produccion: el bundler completo empaqueta TODO, sin optimizeDeps de por medio
vite build

Por eso optimizeDeps no tiene ningún efecto sobre tu bundle de producción: sus ajustes solo moldean cómo se sirven las dependencias mientras desarrollas. Es un detalle fácil de olvidar que ahorra horas de confusión: si buscas por qué una dependencia se empaqueta de cierta forma en tu build final, optimizeDeps no es el sitio donde mirar.

Merece la pena fijar por contraste qué cosas no hace el pre-bundling, porque casi todas las confusiones nacen de atribuirle responsabilidades ajenas:

🚫

No minifica para producción

Empaquetar deps en dev no es tu build final; la minificación y el hashing de assets ocurren en vite build.

🚫

No toca tu src

Tu código se sigue sirviendo módulo a módulo con HMR; el pre-bundling vive del lado de node_modules.

🚫

No hace tree-shaking de tu app

Aplana los módulos internos de cada dep, pero eliminar el código muerto a nivel de aplicación es cosa del bundler de producción.

📝
Preview no es dev

Si quieres ver tu aplicación como será en producción, vite preview sirve el resultado de vite build: sin dev server, sin pre-bundling, sin HMR. Es la herramienta correcta para reproducir un problema que sospechas de producción, precisamente porque apaga toda la maquinaria de desarrollo que este nivel describe. Cuando un fallo aparece en vite pero no en vite preview, ya sabes en qué mitad de Vite mirar.

El pre-bundling es un tratado de paz entre dos épocas de JavaScript

Para apreciar por qué el pre-bundling existe hay que verlo como lo que es: la costura donde se tocan dos eras del lenguaje. Durante quince años, el JavaScript del lado del servidor vivió en CommonJS —require, module.exports, carga síncrona— mientras el estándar ESM tardaba en llegar y en madurar. Cuando el navegador por fin adoptó módulos nativos, heredamos un registro con cientos de miles de paquetes escritos en el dialecto antiguo, más una segunda generación de librerías ESM que, persiguiendo el tree-shaking perfecto, se fragmentaron en tantos módulos que resultan patológicas de servir una a una. El dev server de Vite hace una apuesta radical —no empaquetemos nada, que el navegador cargue ESM directo— y esa apuesta solo es viable porque alguien, en la frontera, traduce el pasado al presente: convierte el CJS heredado en ESM y recompacta el ESM hiperfragmentado en algo que la red no castigue. El pre-bundling es ese traductor. No es un detalle de optimización que podrías ignorar; es la condición de posibilidad de todo el modelo buildless en desarrollo. Sin él, o bien el navegador se atraganta con require, o bien pasa el arranque en una tormenta de peticiones. Con él, la promesa se sostiene: tu código, crudo e instantáneo; el ecosistema entero, reconciliado en silencio justo antes de que lo pidas. Entender esto cambia hasta cómo lees los mensajes de dependencias optimizadas que ves al arrancar: no son ruido, son el momento exacto en que dos épocas de JavaScript firman las paces para que tu dev server pueda existir. Y explica por qué este nivel abre el bloque de optimizeDeps: todo lo que viene después —el escaneo, la caché, las listas de inclusión— son detalles de cómo se firma ese tratado, pero el porqué es este, y sin tenerlo claro los detalles se vuelven conjuros sin sentido.

⚔️ Observa el pre-bundling en acción
  1. Arranca un proyecto Vite y mira la consola: localiza el mensaje de dependencias optimizadas y anota cuánto tarda en frío.
  2. Abre la pestaña de red del navegador en la primera carga y busca las peticiones a node_modules/.vite/deps: cada dependencia grande es una sola petición, no cientos.
  3. Instala lodash-es, importa una función y confirma que la cargas con una petición; imagina la alternativa de seiscientas.
  4. Busca en tu árbol una dependencia que sepas publicada en CommonJS y comprueba que, aun así, la importas con import nativo sin error.
  5. Compara vite con un vite build seguido de vite preview y razona por qué optimizeDeps solo influye en el primero.