wandres.dev
OPTIMIZEDEPS · pre-bundling

Cómo funciona: escaneo, bundle y caché

El pre-bundling ocurre en tres actos: un escaneo que rastrea tus entrypoints para descubrir qué dependencias importas, un empaquetado que Rolldown ejecuta sobre cada una, y un resultado que se cachea en disco y se sirve con cabeceras agresivas. El desglose del pipeline que corre antes de que el navegador pida nada.

⏱ 15 min

El escueto mensaje de dependencias optimizadas que ves al arrancar Vite esconde un pipeline de tres actos. Antes de servir un solo byte al navegador, Vite tiene que responder tres preguntas encadenadas: ¿qué dependencias usas de verdad?, ¿cómo se ve cada una una vez empaquetada a ESM?, y ¿dónde guardo el resultado para no repetir jamás este trabajo? Escaneo, bundle y caché. Entender los tres actos por separado es lo que después te permite razonar sobre cada comportamiento de optimizeDeps en vez de sufrirlo a ciegas.

🎯 Al terminar esta lección sabrás
  • Seguir el pipeline del pre-bundling en sus tres actos: escaneo, bundle y caché.
  • Entender cómo el escaneo descubre las dependencias partiendo de tus entrypoints.
  • Ver por qué Vite 8 empaqueta cada dependencia con Rolldown.
  • Comprender qué guarda node_modules/.vite y cómo se sirve al navegador.

Acto 1: el escaneo de dependencias

Lo primero que hay que desmontar es una intuición falsa: Vite no pre-empaqueta todo node_modules. Sería un desperdicio colosal, porque de los cientos de paquetes instalados tú usas un puñado. Vite empaqueta solo las dependencias que tu código realmente importa, y para saber cuáles son necesita descubrirlas.

El descubrimiento es un escaneo. Vite parte de tus entrypoints —por defecto el index.html, y cualquier script que cuelgue de él— y recorre el grafo de imports hacia dentro. Cada vez que topa con un especificador bare, sin ./ ni ruta relativa, lo anota como dependencia a optimizar. Es un rastreo rápido, movido por el bundler, cuyo único objetivo es cosechar la lista de nombres; no le importa el detalle de tus transformaciones, solo la forma del grafo.

export default defineConfig({
  optimizeDeps: {
    // Si tus entrypoints no son un index.html estandar,
    // se los indicas para que el escaneo los encuentre
    entries: ["src/main.ts", "src/pages/**/*.vue"],
  },
});

Cuando tus entrypoints no son detectables solos —un backend que inyecta el HTML, un patrón de rutas atípico— el campo optimizeDeps.entries le dice al escáner por dónde empezar. Y optimizeDeps.holdUntilCrawlEnd, activo por defecto, controla si Vite espera a terminar todo el rastreo antes de fijar el primer lote de dependencias, buscando el equilibrio entre arrancar pronto y no reoptimizar de más a mitad de sesión.

Hay un matiz decisivo, que reaparecerá en los niveles siguientes: el escaneo es estático. Lee tu código sin ejecutarlo, así que solo ve los imports que puede deducir del texto. Un import() cuyo argumento se decide en tiempo de ejecución queda fuera de su alcance, y esa es la raíz de la mayoría de sorpresas del pre-bundling. Si necesitas desactivar el descubrimiento automático por completo —para controlar la lista a mano— existe optimizeDeps.noDiscovery, que apaga el rastreo y solo optimiza lo que declares explícitamente.

📝
El escaneo es una estimación, no un censo

Que el escaneo sea estático significa que es una estimación: acierta con lo que puede deducir del texto y se queda corto con lo que solo se sabe en ejecución. Vite asume ese margen de error a propósito y lo compensa con la reoptimización en caliente, que corrige sobre la marcha lo que el escaneo no vio. Los niveles siguientes son, en buena medida, sobre cómo estrechar ese margen las pocas veces que molesta.

Acto 2: el bundle con Rolldown

Con la lista en la mano empieza el empaquetado propiamente dicho. Vite toma cada dependencia y la compila a un único módulo ESM: aplana sus módulos internos, convierte a ESM lo que venía en CommonJS y analiza estáticamente sus exportaciones para reconstruir los named exports. Cada dependencia se empaqueta aislada de las demás, como una isla autónoma.

Ese aislamiento no es casual: permite que cada dependencia tenga su propia URL estable y su propia versión de caché, de modo que actualizar una no invalide las demás. Para reconstruir los named exports de un paquete CommonJS, el bundler apoya su análisis en herramientas como el lexer de módulos CJS, que inspecciona el código y deduce qué nombres exporta sin necesidad de ejecutarlo. Es el mismo principio del escaneo llevado al interior de cada paquete: leer, no correr.

El motor que hace este trabajo ha cambiado con los años. Durante mucho tiempo fue esbuild, escrito en Go, elegido justamente porque empaquetar dependencias es una tarea donde la velocidad bruta manda y la fidelidad de plugins importa poco. En Vite 8, Rolldown —el bundler en Rust construido sobre Oxc— asume tanto el pre-bundling en desarrollo como el empaquetado de producción, unificando en un solo motor lo que antes eran dos herramientas distintas.

export default defineConfig({
  optimizeDeps: {
    // Los ajustes de bajo nivel del bundler de deps:
    // bajo Rolldown se llaman rollupOptions (antes: esbuildOptions)
    rollupOptions: {
      // resolver extensiones raras, definir globals, etc.
    },
  },
});

Ese relevo tiene una consecuencia práctica visible en la config: los ajustes de bajo nivel que antes vivían en optimizeDeps.esbuildOptions pasan a optimizeDeps.rollupOptions bajo Rolldown. Es la lección recurrente de todo el track en miniatura: la interfaz de optimizeDeps que aprendes aquí perdura, mientras el motor que hay debajo es la pieza intercambiable que mejora sola sin pedirte nada.

¿Por qué un bundler rápido y no el Rollup de producción con toda su fidelidad de plugins? Porque las dos tareas tienen prioridades opuestas. El bundle de producción se ejecuta una vez antes de desplegar y puede permitirse ser minucioso: aplica cada plugin, optimiza al máximo, tarda lo que haga falta. El pre-bundling se ejecuta al arrancar y bloquea tu primera interacción, así que lo que manda es la latencia. Reconciliar dependencias no exige la fidelidad completa del pipeline de producción, y por eso históricamente se delegó en un motor optimizado para velocidad bruta; con Rolldown, ese mismo motor rápido sirve además para producción, y la vieja dualidad esbuild-para-dev y Rollup-para-build se disuelve.

Acto 3: el resultado cacheado

El bundle no se sirve al vuelo: se escribe en disco, en node_modules/.vite/deps. Allí aparece un archivo por dependencia optimizada, más un _metadata.json que registra la huella de la caché y el mapa de qué dependencia corresponde a qué archivo.

node_modules/.vite/deps/
├─ _metadata.json      # huella + mapa de dependencias optimizadas
├─ react.js            # una dep = un modulo ESM ya empaquetado
├─ react-dom_client.js
└─ lodash-es.js        # 600 modulos internos, aplanados en uno

El último eslabón es la reescritura. Cuando el navegador pide un archivo tuyo que importaba debounce desde lodash-es, Vite ya ha reescrito ese import para que apunte al artefacto cacheado, con una versión colgada en la URL para el control de caché. El navegador nunca resuelve lodash-es por su cuenta: recibe una ruta directa al bundle en deps, servida con cabeceras agresivas porque ese archivo no va a cambiar mientras la dependencia sea la misma.

// Lo que escribes tu
import { debounce } from "lodash-es";

// Lo que el navegador recibe de verdad, ya reescrito por Vite
import { debounce } from "/node_modules/.vite/deps/lodash-es.js?v=8a1c2f";

Esa versión en la URL es un hash que Vite recalcula cada vez que reoptimiza. Mientras no cambie, el navegador puede cachear el archivo con total agresividad, porque una URL distinta significaría siempre contenido distinto. Cuando la reoptimización cambia el hash, la URL cambia con él, y el navegador se ve obligado a pedir la versión nueva: la caché se invalida sola por el simple hecho de que el nombre ya no coincide.

Esta reescritura es completamente transparente para ti: nunca escribes rutas a deps ni versiones a mano. Tú importas por el nombre del paquete, como siempre, y Vite traduce ese nombre a la ruta cacheada correcta en cada petición. Es la misma filosofía de todo el pre-bundling —hacer el trabajo sucio en la frontera para que tu código no tenga que enterarse— aplicada al último eslabón de la cadena.

flowchart LR
A[Entrypoints] --> B[Acto 1: escaneo de imports bare]
B --> C[Lista de dependencias usadas]
C --> D[Acto 2: bundle con Rolldown]
D --> E[Acto 3: escritura en disco de deps]
E --> F[Reescritura de imports en tu codigo]
F --> G[Servido al navegador con cache fuerte]
style D fill:#cba6f7,color:#11111b
style E fill:#f9e2af,color:#11111b
style G fill:#a6e3a1,color:#11111b
💡
Cada entorno tiene su propia caché

Con la Environment API de Vite, el cliente y el SSR se optimizan por separado: verás node_modules/.vite/deps para el navegador y una carpeta hermana para el entorno de servidor. Tiene sentido, porque una misma dependencia puede necesitar un tratamiento distinto según se ejecute en el navegador o en Node. Pensar en “la caché de deps” en singular es una simplificación cómoda; en realidad hay una por entorno.

Los tres actos, de un vistazo

🔍

Escaneo

Rastrea desde tus entrypoints y cosecha la lista de dependencias que de verdad importas. Ni una más.

🦀

Bundle

Rolldown empaqueta cada dependencia a un módulo ESM aislado: aplana, convierte CJS y reconstruye named exports.

🧊

Caché

El resultado se escribe en node_modules/.vite/deps y se sirve con versión y cabeceras agresivas. Se hace una vez.

Con estos tres actos nombrados tienes ya el vocabulario para todo lo que sigue. Los niveles posteriores no introducen mecanismos nuevos: te enseñan a intervenir en uno u otro acto cuando el automático no acierta —forzar el escaneo con include, esquivarlo con exclude, rehacer la caché con --force—. Todo cuelga de este esqueleto de análisis, transformación y memoización.

Dónde falla cada acto

Como los tres actos son independientes, cada uno falla a su manera y se arregla con una herramienta distinta. Tener presente esta correspondencia es lo que convierte un síntoma difuso en una acción concreta, sin pasar por el ensayo y error.

Acto Cuando falla, el síntoma es La herramienta es
Escaneo una dep aparece tarde, con recarga optimizeDeps.include
Bundle named exports rotos de un paquete CJS include o el interop explícito
Caché sigues viendo lo viejo tras un cambio la opción --force

Ninguna de estas herramientas es un truco aislado: cada una interviene en un acto concreto del pipeline, y elegir la correcta empieza siempre por identificar en cuál de los tres se rompió la cadena. Ese hábito —localizar el acto antes de tocar nada— es la diferencia entre depurar con método y probar remedios al azar.

El pre-bundling es un compilador con tres fases clásicas

Vale la pena reconocer en este pipeline la silueta de algo muy antiguo: es la estructura de un compilador. El escaneo es el análisis —recorrer el código para descubrir de qué depende, exactamente como un compilador construye su tabla de símbolos rastreando referencias—. El bundle es la transformación y generación de código —tomar cada dependencia y emitir una forma canónica, ESM aplanado, que el consumidor pueda ingerir sin fricción—. Y la caché es la memoización que todo compilador serio acaba necesitando para no rehacer trabajo idéntico entre ejecuciones. Ver el pre-bundling así, y no como una caja negra que optimiza cosas, cambia por completo tu capacidad de depurarlo. Cuando una dependencia no aparece optimizada, sabes que el fallo está en el acto uno: el escaneo no la vio, y la pregunta es por qué tu grafo de imports no la alcanza desde un entrypoint conocido. Cuando una dependencia se empaqueta pero sus named exports salen rotos, sabes que el fallo está en el acto dos: el análisis de exportaciones no pudo reconstruir la interfaz. Y cuando cambias algo y Vite sigue sirviendo lo viejo, sabes que el fallo está en el acto tres: la huella de la caché no capturó tu cambio. Cada uno de los tres actos falla de una forma característica y se arregla con una herramienta distinta, y casi todo el contenido de los niveles siguientes —include, exclude, la opción --force, los problemas de monorepo— no es más que aprender a intervenir con precisión en el acto correcto. Quien entiende que el pre-bundling es análisis, transformación y memoización deja de tratarlo como magia y empieza a tratarlo como lo que es: un compilador pequeño, especializado y perfectamente razonable, que corre en el umbral entre tu código y el navegador. Y esa perspectiva tiene un beneficio secundario nada trivial: al reconocer estructuras clásicas de compilación en una herramienta moderna, transfieres a ella toda la intuición que ya tienes sobre cómo fallan los compiladores, en lugar de aprender su fenomenología desde cero.

⚔️ Radiografía del pipeline
  1. Borra node_modules/.vite, arranca Vite y cronometra el arranque: ese es el coste del escaneo más el bundle en frío.
  2. Abre node_modules/.vite/deps y lee el _metadata.json: identifica la huella y el mapa de dependencias optimizadas.
  3. Cuenta cuántos archivos hay en deps y compáralo con cuántas dependencias tiene tu package.json: verás que solo se empaquetó lo que importas.
  4. Localiza en la pestaña de red la URL con versión de una dependencia optimizada y explica por qué esa versión permite cachear con agresividad.
  5. Fuerza un entrypoint atípico con optimizeDeps.entries y comprueba que el escaneo descubre dependencias que antes se le escapaban.