include y exclude: forzar o evitar el pre-bundling
El escaneo automático acierta casi siempre, pero no es infalible. optimizeDeps.include fuerza el pre-bundling de un paquete que el escaneo no vio o que llegaría tarde; optimizeDeps.exclude lo deja fuera cuando ya es ESM limpio o cuando lo estás desarrollando en vivo. Cuándo y por qué tocar cada lista, y por qué debería ser la excepción.
El escaneo automático acierta la inmensa mayoría de las veces: la prueba es que la mayoría de proyectos jamás tocan optimizeDeps. Pero ninguna heurística es infalible, y Vite deja dos válvulas de escape para cuando la suya se equivoca. include fuerza a que un paquete se pre-empaquete; exclude impide que se pre-empaquete. Conocer el puñado de situaciones concretas que justifican cada una es lo que separa dirigir a Vite de pelearte con él a ciegas.
- Usar
optimizeDeps.includepara forzar el pre-bundling de un paquete. - Usar
optimizeDeps.excludepara dejar un paquete fuera del pre-bundling. - Reconocer los casos reales que exigen cada lista: imports tardíos, deps anidadas, paquetes enlazados.
- Entender por qué tocar estas listas debe ser la excepción, no el hábito.
include: forzar el pre-bundling
optimizeDeps.include es una lista de paquetes que Vite empaquetará sí o sí, los haya visto el escaneo o no. Su razón de ser es que el rastreo estático tiene puntos ciegos, y sin include esos puntos ciegos se pagan con una reoptimización a mitad de sesión que interrumpe tu trabajo con una recarga de página.
export default defineConfig({
optimizeDeps: {
include: [
"libreria-pesada", // llega solo por un import dinamico tardio
"paquete-padre > anidada", // una dep anidada que importas en profundidad
"mi-lib/feature", // un subpath concreto de un paquete
],
},
});
Los casos que lo piden son reconocibles. El primero es el de los imports dinámicos tardíos: si una dependencia solo se alcanza por un import() que el escaneo no rastrea hasta que el navegador lo ejecuta, aparecerá tarde, y al descubrirla en caliente Vite se detiene, la optimiza y recarga la página. Anticiparla en include la empaqueta desde el arranque y elimina ese parón. Es, con diferencia, el motivo más común para tocar esta lista.
El segundo es el de las dependencias anidadas. A veces importas en profundidad una dependencia de una dependencia, que no figura en tu package.json. El escáner puede no atribuirla bien, y include la fija con su sintaxis de anidación, nombrando el paquete padre y la hija. El tercero son los named exports de un paquete CJS: forzar el include de un paquete CommonJS problemático lo somete al tratamiento completo de interoperabilidad, que a menudo arregla unos named exports que, servidos de otro modo, saldrían rotos.
En los tres casos el patrón de fondo es el mismo: el escaneo automático no llegó a tiempo, o no llegó bien, y include suple ese fallo declarando la intención por adelantado. No estás cambiando qué hace Vite, sino cuándo lo hace: adelantas al arranque un trabajo que, si no, ocurriría tarde y con interrupción.
Import dinámico tardío
Una dep que solo se alcanza por un import() en caliente. Sin include, provoca un parón con recarga la primera vez que se ejecuta.
Dependencia anidada
Importas en profundidad una dep de una dep que no está en tu package.json. include la fija con su sintaxis de anidación.
Named exports de CJS
Un paquete CommonJS cuyos nombres salen rotos. Forzar el include le da el tratamiento completo de interoperabilidad.
exclude: evitar el pre-bundling
optimizeDeps.exclude es la lista opuesta: paquetes que Vite no debe empaquetar, aunque tu código los importe. El navegador los recibirá tal como vienen de node_modules, sin pasar por el bundler de dependencias.
export default defineConfig({
optimizeDeps: {
exclude: [
"paquete-del-workspace", // lo estas editando en vivo y quieres HMR
"dep-esm-limpia", // ya es ESM de pocos modulos, no gana nada
],
},
});
El caso estrella es el paquete que estás desarrollando en vivo, típicamente uno enlazado de tu monorepo. Si Vite lo pre-empaqueta, lo congela en un artefacto cacheado, y tus ediciones dejan de reflejarse hasta que se reoptimice. Excluirlo lo mantiene vivo: el dev server lo sirve como fuente y el HMR vuelve a funcionar sobre él, guardado a guardado.
El otro caso, menos frecuente, es una dependencia ya publicada como ESM limpio y con pocos módulos, donde el pre-bundling no aporta nada que compense el paso. Aquí exclude es más una micro-optimización que una necesidad, y conviene medir antes de asumir que ganas algo: pre-empaquetar una dependencia pequeña cuesta tan poco que excluirla rara vez merece la complejidad añadida a la config.
Hay un tercer caso, más raro, que conviene reconocer: una dependencia que hace algo especial en tiempo de ejecución y no tolera ser reempaquetada —por ejemplo, una que resuelve rutas relativas a su propia ubicación en disco, o que registra un worker esperando una estructura de archivos concreta—. Reempaquetarla rompería esas suposiciones, y exclude la deja intacta. Es infrecuente, pero cuando aparece, el síntoma es un error que solo cobra sentido si sabes que el bundler movió los archivos de sitio.
Excluir un paquete reintroduce justo los problemas que el pre-bundling resolvía. Si el paquete excluido trae subdependencias en CommonJS o estalla en cientos de módulos, volverás a ver un require que el navegador no entiende o una cascada de peticiones. Por eso exclude rara vez va solo: lo habitual es excluir el paquete enlazado que editas y, a la vez, include de sus subdependencias CJS para que esas sí se empaqueten. Excluir sin pensar en las subdependencias es cambiar un problema por otro.
El patrón combinado del monorepo
La combinación de las dos listas es tan frecuente en monorepos que merece verse como un patrón con nombre. Quieres editar en vivo tu paquete interno —por eso lo excluyes— pero ese paquete arrastra dependencias que sí necesitan el tratamiento del pre-bundling. La solución es aplicar cada lista a su capa:
export default defineConfig({
optimizeDeps: {
exclude: ["@acme/ui"], // tu paquete: fuente viva, con HMR
include: ["@acme/ui > react-icons"], // su dep CJS: empaquetada aparte
},
});
Leído así, no hay contradicción entre excluir e incluir a la vez: operan sobre niveles distintos del árbol. exclude gobierna tu paquete, que quieres tocar; include gobierna lo que ese paquete consume y que no vas a tocar. Es la expresión exacta, en dos líneas de config, de la frontera entre “código mío en desarrollo” y “dependencia estable de terceros”.
Vale la pena subrayar que este patrón es específico de los monorepos, no un ajuste que debas replicar en cualquier proyecto. En una app sin paquetes internos enlazados, ni exclude ni el include de subdependencias tienen sentido: el automático basta. El patrón nace de una tensión que solo existe cuando desarrollas a la vez la app y las librerías que consume.
Cuando lo veas en la config de un monorepo ajeno, ya sabrás leerlo: no es una contradicción ni un parche improvisado, sino la codificación deliberada de qué código está vivo y cuál es dependencia estable. Esa lectura te ahorra el impulso de borrarlo por parecer redundante.
La decisión, en un diagrama
La lógica de cuándo tocar cada lista cabe en un solo diagrama de flujo, y conviene tenerla presente antes de escribir nada en la config:
flowchart TD Q[Vite va a servir una dependencia] --> A[La vio el escaneo] A -->|no, y llega tarde| I[include: empaquetala desde el arranque] A -->|si, y la editas en vivo| E[exclude: sirvela como fuente para HMR] A -->|si, y no la editas| D[deja actuar al automatico] style I fill:#89b4fa,color:#11111b style E fill:#f9e2af,color:#11111b style D fill:#a6e3a1,color:#11111b
Las dos listas y sus disparadores caben también en una tabla que puedes usar como chuleta rápida cuando aparezca el síntoma:
| Lista | Qué hace | Señal de que la necesitas |
|---|---|---|
include |
fuerza el pre-bundling de un paquete | un parón con recarga a media sesión |
exclude |
deja un paquete fuera del pre-bundling | un paquete enlazado cuyos cambios no se reflejan |
Ninguna de estas dos listas es un punto de partida. El punto de partida es no tocar nada: arranca, desarrolla, y deja que el descubrimiento y la reoptimización en caliente hagan su trabajo. Solo cuando un síntoma concreto y repetible aparezca —un parón que te molesta, un paquete que no refresca— sacas el bisturí y añades la entrada mínima que lo cura. El orden importa: quien empieza configurando optimizeDeps casi siempre configura de más.
La regla: tocar estas listas es la excepción
Conviene terminar con una advertencia de sobriedad. El descubrimiento automático más la reoptimización en caliente resuelven, solos, la gran mayoría de los casos. include y exclude no son perillas que debas ajustar de rutina en cada proyecto; son parches quirúrgicos para cuando la heurística falla de una forma concreta y reproducible.
La señal de que necesitas include suele ser un parón con recarga a media sesión que te molesta y quieres eliminar. La señal de que necesitas exclude suele ser un paquete enlazado cuyas ediciones no se reflejan. Fuera de esos síntomas claros, añadir paquetes a estas listas por si acaso solo añade config que mantener y una oportunidad más de que algo diverja sin que nadie recuerde por qué.
Hay un caso extremo que conviene conocer aunque rara vez uses: optimizeDeps.noDiscovery apaga por completo el descubrimiento automático, de modo que Vite solo optimiza lo que declares en include. Es el modo de control total, útil en configuraciones muy afinadas donde prefieres una lista exhaustiva y predecible a la heurística. Pero es también la forma más rápida de dispararte en el pie: olvidar una dependencia en la lista significa que no se optimiza en absoluto, con todos los males del primer nivel de vuelta.
La configuración sana de optimizeDeps tiende a cero. Empieza sin tocar nada y deja que el automático trabaje; añade una entrada solo cuando un síntoma concreto te obligue, y documéntala con un comentario que diga por qué. Una lista que crece por acumulación defensiva —entradas puestas por si acaso, copiadas de otro proyecto, heredadas de un problema ya resuelto— es deuda técnica disfrazada de configuración. La disciplina no es rellenarla bien, sino resistirse a rellenarla.
En la práctica, un proyecto sano puede vivir años con optimizeDeps completamente vacío. Ese vacío no es una carencia: es la señal de que el automático está acertando y de que no has tenido que corregirlo. Cuando por fin escribas una entrada, que sea porque un síntoma te lo pidió, no porque una guía te dijera que rellenaras la sección.
Hay una lección general escondida en estas dos listas, y es sobre la relación entre un sistema automático y el humano que lo supervisa. El escaneo de dependencias de Vite es una heurística: construye un modelo de tu grafo de imports rastreando lo que puede ver estáticamente desde tus entrypoints. Ese modelo es certero casi siempre, pero tiene fronteras conocidas —no ejecuta tus imports dinámicos, no adivina qué subdependencia importarás en profundidad, no sabe cuál de tus paquetes enlazados estás editando ahora mismo—. include y exclude son, literalmente, la interfaz por la que tú, que sí sabes esas cosas, corriges el modelo del sistema. include dice sé que usaré esto aunque tú no lo veas todavía; exclude dice sé que esto lo estoy tocando, no lo congeles. No estás peleándote con Vite: estás aportando la información que a Vite le falta por construcción, porque vive en tu cabeza y no en el código estático que puede analizar. Verlo así reordena la actitud correcta. El error de novato es tratar estas listas como un panel de ajustes que hay que rellenar para configurar bien el pre-bundling, y acaban llenas de entradas defensivas que nadie sabe justificar. El instinto de ingeniero senior es el contrario: cada entrada en include o exclude es una afirmación sobre algo que sabes y la heurística no, y por tanto debe poder explicarse en una frase. Si no puedes decir qué punto ciego del escaneo estás cubriendo, esa entrada no debería existir. Las mejores configuraciones de optimizeDeps no son las más completas, sino las más vacías posibles: tantas entradas como puntos ciegos reales tengas, ni una más. La disciplina no consiste en acumular ajustes, sino en saber exactamente por qué está ahí cada uno de los pocos que conservas, de modo que dentro de un año, cuando el proyecto haya cambiado, puedas releer la lista y decidir con criterio qué sigue haciendo falta y qué era una cicatriz de un problema ya olvidado.
- Provoca una reoptimización en caliente: carga una dependencia solo por un
import()tras una interacción y observa el parón con recarga en consola. - Mete esa dependencia en
optimizeDeps.includey confirma que ahora se empaqueta al arrancar, sin parón posterior. - Enlaza un paquete de un workspace, edítalo y observa si tus cambios se reflejan; si no, añádelo a
optimizeDeps.excludey vuelve a probar. - Si ese paquete enlazado trae una subdependencia CommonJS, aplica el patrón combinado:
excludedel paquete eincludede la subdependencia. - Revisa una config real y, por cada entrada de
includeoexclude, escribe en una frase el punto ciego que cubre; borra las que no sepas justificar.