Vendor splitting y dedupe: dependencias cacheables sin duplicados
Separar las dependencias de terceros en su propio chunk es la jugada de caché más rentable del frontend: cambian despacio, así que tus despliegues diarios no las invalidan. Pero la estrategia se envenena con un enemigo silencioso, la duplicación: dos copias de la misma librería en el grafo por rangos de versión incompatibles. Configurar el vendor splitting en Rolldown, cazar duplicados con pnpm y resolve.dedupe, y entender por qué deduplicar un módulo con estado no es ahorro sino corrección.
Tu código cambia cada día; tu framework, cada pocas semanas. Mezclar ambos en un mismo chunk es tirar caché a la basura: cada corrección de una línea tuya invalida también todos los bytes de las librerías, obligando al usuario a redescargarlas. Separar las dependencias de terceros en un vendor chunk aprovecha su lentitud de cambio para que sobreviva en la caché del navegador a decenas de tus despliegues. Pero esa estrategia tiene un enemigo silencioso que la corroe desde dentro: la duplicación. Cuando la misma librería aparece dos veces en el grafo, no solo pagas sus bytes dos veces —a veces rompes la aplicación—. Este nivel es sobre las dos caras de la misma moneda: separar bien lo cacheable y eliminar lo que sobra.
- Configurar el vendor splitting en Vite 8 con Rolldown mediante
advancedChunksy su forma clásica. - Detectar dependencias duplicadas en el grafo con el treemap y con
pnpm why. - Consolidar versiones con
overrides,pnpm dedupeyresolve.dedupede Vite. - Entender por qué deduplicar un módulo con estado es una cuestión de corrección, no solo de peso.
Separar el vendor para la caché
Rolldown extrae código común por defecto, pero el vendor merece una regla explícita porque quieres controlar su granularidad. La API moderna de Vite 8 es advancedChunks, que define grupos por un test sobre la ruta del módulo, con umbrales de tamaño para evitar chunks minúsculos. La forma clásica manualChunks, una función que devuelve el nombre del chunk, sigue soportada por compatibilidad.
// vite.config.ts — vendor subdividido por familia con advancedChunks
import { defineConfig } from "vite";
export default defineConfig({
build: {
rollupOptions: {
output: {
advancedChunks: {
minSize: 20_000,
groups: [
{ name: "vendor-react", test: /node_modules\/(react|react-dom)\// },
{ name: "vendor-charts", test: /node_modules\/(d3|visx)\// },
{ name: "vendor", test: /node_modules\// },
],
},
},
},
},
});
El orden importa: el primer grupo cuyo test casa gana, así que las familias específicas van antes que el vendor genérico que recoge el resto. Aíslas lo grande y volátil —el framework, la librería de gráficas— en chunks propios y fundes lo demás en un vendor común. La razón es la caché: cuando actualizas una sola librería, solo cambia el hash de su chunk, y el navegador reutiliza todos los demás. Un vendor monolítico invalidaría el bloque entero por una sola actualización.
La trampa clásica del splitting manual es arrastrar al vendor inicial una dependencia que solo usa una ruta diferida. El automático la colocaría por alcanzabilidad en el chunk asíncrono de esa pantalla; una regla manualChunks torpe la fuerza al camino crítico, y el usuario descarga en el arranque una librería que no verá hasta pulsar un botón secundario. Antes de fijar una regla, comprueba en el treemap que la dependencia de verdad la comparten muchas rutas iniciales.
Cazar duplicados en el grafo
El vendor bien separado se envenena si la misma dependencia entra dos veces. Ocurre por la resolución de versiones: el paquete A pide util@^4.0 y el paquete B pide util@^4.2, y si los rangos no convergen en una única versión instalada, el gestor deja dos copias en node_modules bajo rutas distintas. El bundler, fiel al grafo, empaqueta ambas. En el treemap las ves como dos bloques gemelos con el mismo nombre.
flowchart TD app[Tu aplicacion] --> a[libreria A pide util 4.0] app --> b[libreria B pide util 4.2] a --> u1[copia util 4.0] b --> u2[copia util 4.2] u1 -. mismo paquete dos veces .- u2 style u1 fill:#f38ba8,color:#11111b style u2 fill:#f38ba8,color:#11111b
La primera herramienta es el gestor de paquetes. pnpm why util te dice quién pide cada versión y por qué conviven; pnpm dedupe reescribe el árbol para colapsar copias compatibles en una sola, y pnpm dedupe --check falla en CI si quedan duplicados evitables. Cuando dos rangos son irreconciliables y ninguno cede, fuerzas la mano con un override, que impone una versión única a todo el árbol.
// package.json — forzar una unica version en todo el grafo con pnpm
{
"pnpm": {
"overrides": {
"util": "4.2.0"
}
}
}
Aunque node_modules tenga dos copias, Vite puede resolver ciertos paquetes a una sola instancia con resolve.dedupe. Es la red de seguridad para los singletons: aunque una dependencia transitiva arrastre su propia copia de tu framework, el bundler la colapsa a la del proyecto raíz. No sustituye al dedupe del gestor, pero rescata los casos donde el árbol físico no se puede aplanar del todo.
// vite.config.ts — colapsar singletons a una sola instancia
export default defineConfig({
resolve: {
dedupe: ["react", "react-dom"],
},
});
Singletons: cuando duplicar es un bug
Para la mayoría de librerías, un duplicado es solo peso desperdiciado. Pero para una clase especial —los singletons con estado— es un fallo de corrección. React es el ejemplo canónico: su sistema de hooks y de contexto depende de que haya una única instancia del módulo en toda la aplicación. Con dos copias, un componente registra su contexto en una y otro lo lee de la otra; el valor no aparece, los hooks lanzan errores crípticos sobre múltiples versiones, y el bug no se parece en nada a un problema de tamaño.
La raíz es que un módulo no es solo código: es también identidad. Dos copias de un módulo con estado son dos universos que no se ven entre sí, cada uno con su propio store, su propio contexto, su propio registro. Por eso las librerías que son singletons se declaran como peerDependencies —“no me empaquetes tú, usa la del anfitrión”— y por eso deduplicarlas no es una optimización opcional sino una condición para que la aplicación funcione.
Frameworks de UI
React, Vue o Solid guardan estado global —contexto, reactividad— que exige una sola copia. Dos instancias rompen los hooks y la propagación.
Sistemas de estilo
Librerías de CSS-in-JS con un registro de estilos compartido. Dos copias generan dos registros y estilos que se pisan o desaparecen.
Stores y clientes
Un cliente de datos o un store global cacheado. Duplicarlo parte la caché en dos y los componentes leen de mitades distintas.
La lección honda del dedupe es que un bundle no es una bolsa de bytes que quieres encoger, sino un espacio de nombres donde cada módulo tiene una identidad que debe preservarse. Cuando pensamos en deduplicar solemos verlo como compresión —quitar copias para pesar menos—, y para el código sin estado esa intuición basta. Pero para el código con estado la deduplicación es algo cualitativamente distinto: es garantizar que “el módulo X” se refiera a una y solo una entidad en todo el sistema, porque su corrección depende de que todos los que hablan de X hablen del mismo X. Dos copias de un módulo puro son un desperdicio; dos copias de un módulo con estado son una esquizofrenia —dos realidades paralelas que divergen en silencio hasta que un síntoma inexplicable aflora lejos de la causa—. Esta es la misma idea que en un sistema operativo hace que solo haya un kernel, que en una base de datos exige una única fuente de verdad para cada dato, que en un lenguaje con módulos garantiza que importar el mismo módulo dos veces devuelva la misma instancia. El bundler es, en el fondo, un pequeño enlazador que resuelve nombres a definiciones, y su trabajo silencioso más importante no es hacer el resultado pequeño sino hacerlo coherente: que el grafo lógico de “quién depende de quién” se preserve en el artefacto físico sin fracturar identidades. Cuando fuerzas un manualChunks que separa lo que debía ir junto, o cuando dejas que dos versiones de un singleton coexistan, no estás cometiendo una ineficiencia: estás rompiendo el contrato de identidad del que depende la semántica de tu programa. Quien entiende esto deja de ver el dedupe como una micro-optimización y empieza a verlo como lo que es: la disciplina de mantener un único significado para cada nombre en un sistema que ensambla miles de piezas escritas por miles de manos.
- Configura
advancedChunkspara aislar tu framework en un chunk y fundir el resto denode_modulesen un vendor común. - Cambia una línea de tu código de app, reconstruye y confirma que el hash del vendor chunk no se movió.
- Busca en el treemap un bloque duplicado y usa
pnpm whypara descubrir qué dos paquetes piden versiones incompatibles. - Consolida esa versión con
pnpm dedupeo unoverride, reconstruye y verifica que el duplicado desapareció del mapa. - Añade tu framework a
resolve.dedupey razona por qué, para un singleton con estado, esa línea es corrección y no solo ahorro.