Chunks compartidos y vendor
Si dos rutas divididas importan la misma librería, un bundler ingenuo la duplicaría en ambos chunks. La extracción de código común la promueve a un chunk compartido que se descarga una vez y se cachea; separar las dependencias de terceros en un vendor chunk lleva esa idea a su forma más rentable para la caché de larga duración.
Divides tu app por rutas y cada ruta se vuelve un chunk. Pero dos rutas comparten cosas: la librería de fechas, el cliente HTTP, el set de componentes de UI. Si cada chunk arrastrara su propia copia, el usuario descargaría la misma librería una vez por ruta visitada. La solución es la extracción de código común: el bundler detecta lo que varios chunks necesitan y lo eleva a un chunk compartido, descargado una vez y reutilizado. Llevado a las dependencias de terceros, ese mismo mecanismo produce el vendor chunk, la pieza clave de una buena estrategia de caché.
- Entender el problema de la duplicación de código entre chunks divididos.
- Ver cómo el bundler extrae automáticamente lo compartido según la alcanzabilidad.
- Distinguir vendor chunks de app chunks y por qué difieren en ritmo de cambio.
- Conectar el hashing de contenido con la caché inmutable de larga duración.
El problema de la duplicación
Un import() propone una partición del grafo, pero varias particiones se solapan. Imagina dos rutas, /informe y /factura, cada una en su chunk, y ambas importan una librería de formato de moneda. Un bundler que tratara cada corte de forma aislada incluiría la librería dos veces, una en cada chunk. El usuario que visita las dos rutas la descarga, parsea y ejecuta dos veces. Peor aún: son dos copias distintas en memoria, con su propio estado.
La extracción de código común existe para evitarlo. En lugar de duplicar, el bundler reconoce que un módulo es alcanzable desde más de un punto de entrada de chunk y lo saca a un tercer chunk que ambos cargan. La regla que gobierna esto es la misma alcanzabilidad del nivel anterior, aplicada a la inversa: un módulo compartido por N chunks no pertenece a ninguno, así que se emancipa.
flowchart TD E[entry] -.import dinamico.-> R1[ruta informe] E -.import dinamico.-> R2[ruta factura] R1 --> S[formato moneda compartido] R2 --> S S --> D[dependencia interna] style S fill:#89b4fa,color:#11111b style R1 fill:#f9e2af,color:#11111b style R2 fill:#f9e2af,color:#11111b
Extracción automática de código común
Rollup y Rolldown hacen esta extracción por defecto, sin configuración. Su algoritmo de chunking analiza el grafo completo y agrupa los módulos por el conjunto de entradas desde las que son alcanzables: todos los módulos que comparten exactamente el mismo conjunto de orígenes tienden a acabar juntos en un chunk. Es un cálculo de teoría de grafos, no una lista de reglas que tú escribas.
Un error común es imaginar que los chunks siguen la estructura de carpetas de tu proyecto. No: siguen el grafo de alcanzabilidad. Dos archivos en carpetas opuestas acaban en el mismo chunk si los alcanzan las mismas entradas, y dos archivos vecinos se separan si uno es compartido y el otro no. El bundler razona sobre quién importa a quién, no sobre dónde vive cada archivo.
El efecto es que la duplicación desaparece casi sola. Cuando ves en tu build un chunk con un nombre como chunk-x7k2.js que no corresponde a ninguna ruta, suele ser exactamente esto: código que varias partes de tu app comparten y que el bundler ha emancipado para no repetir. No es basura ni un fallo; es la optimización trabajando.
Formalmente, el algoritmo asigna a cada módulo una firma: el conjunto de entradas desde las que es alcanzable. Todos los módulos con la misma firma pueden vivir juntos sin duplicarse, porque se cargan exactamente en las mismas circunstancias. Un módulo alcanzable solo desde la ruta A va al chunk de A; uno alcanzable desde A y B va a un chunk compartido por ambas; uno alcanzable desde todas las rutas tiende hacia el vendor o un común global. El chunking automático es, en esencia, agrupar el grafo por esta firma de alcanzabilidad.
Pero emancipar tiene un coste, y aquí aparece el primer equilibrio del splitting. Un chunk compartido es una petición más que el navegador debe hacer al cargar cualquier ruta que dependa de él. Deduplicar 40 KB compartidos por dos rutas casi siempre gana; extraer 200 bytes que comparten dos rutas casi nunca, porque el ahorro de bytes no cubre el coste fijo de una petición extra. Por eso los bundlers no extraen todo lo compartido de forma indiscriminada: aplican un umbral de tamaño mínimo por debajo del cual prefieren duplicar un módulo diminuto antes que pagar por un chunk propio.
Cada chunk compartido cambia una copia duplicada por una petición coordinada. La apuesta sale a cuenta cuando el código deduplicado es grande y lo comparten muchas rutas, y sale mal cuando es minúsculo o lo comparten solo dos pantallas que rara vez se visitan juntas. El bundler decide por ti con heurísticas de tamaño, pero saber que hay una apuesta debajo te prepara para el nivel final, donde el over-splitting es justamente esta apuesta perdida a gran escala.
Vendor: separar lo que cambia despacio
Entre todo lo compartido hay una categoría especial: las dependencias de terceros, tu node_modules. Tienen una propiedad que las hace ideales para aislar en su propio chunk —el vendor chunk—: cambian mucho más despacio que tu código. Tú editas componentes cada día; la versión de tu framework o de tu librería de fechas cambia cada pocas semanas.
Esa diferencia de ritmo es oro para la caché. Si mezclas tu código y el de terceros en el mismo chunk, cada vez que corriges una línea tuya invalidas también todos los bytes de las librerías, obligando al usuario a redescargarlas. Si las separas, tus despliegues frecuentes solo invalidan el chunk de app, y el vendor chunk permanece en la caché del navegador intacto durante semanas.
Queda una decisión de grano: ¿un único vendor chunk gigante, o uno por familia de dependencias? El vendor monolítico es simple pero frágil ante la caché —actualizar una sola librería invalida el bloque entero, incluidas las demás que no cambiaron—. Subdividir el vendor por familias grandes —el framework por un lado, la librería de gráficas por otro— acota la invalidación a lo que de verdad se movió, a cambio de más peticiones. La regla en 2026 es aislar las dependencias grandes y volátiles en su propio chunk y fundir el resto en un vendor común:
// vite.config.ts — vendor subdividido por familia con la forma funcion
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (!id.includes("node_modules")) return;
if (id.includes("react")) return "vendor-react";
if (id.includes("d3") || id.includes("visx")) return "vendor-charts";
return "vendor";
},
},
},
},
});
El compromiso entre ambas granularidades se resume así:
| Estrategia vendor | Ventaja | Coste |
|---|---|---|
| Vendor único | una sola petición, configuración trivial | una dependencia nueva invalida todo el bloque |
| Vendor por familia | invalidación acotada, mejor caché a largo plazo | más peticiones en el arranque |
Vendor chunk
Código de node_modules. Cambia despacio, se cachea largo tiempo, sobrevive a tus despliegues diarios.
App chunk
Tu propio código. Cambia a diario, se invalida en cada despliegue, es la parte volátil.
Shared chunk
Código compartido entre rutas, sea tuyo o de terceros. Se descarga una vez y sirve a varias pantallas.
Una dependencia que solo usa una ruta diferida no pertenece al vendor que todos cargan al arrancar: debe viajar en el chunk asíncrono de esa ruta. El automático lo resuelve por alcanzabilidad, pero las reglas manuales de vendor lo rompen a menudo, arrastrando al camino crítico librerías que solo hacían falta en una pantalla secundaria. Vendor significa terceros, no inicial, y cada dependencia va donde su uso la coloca.
Hashing de contenido y caché inmutable
La separación solo rinde si el navegador puede cachear cada chunk con total agresividad, y para eso el bundler pone un hash del contenido en el nombre de cada archivo: vendor-9f3a1c.js. El hash es una huella de los bytes; si el contenido no cambia, el nombre no cambia, y el navegador reutiliza el archivo cacheado sin ni siquiera preguntar al servidor.
// vite.config.ts — el patron de nombre lleva el hash de contenido
export default defineConfig({
build: {
rollupOptions: {
output: {
entryFileNames: "assets/[name]-[hash].js",
chunkFileNames: "assets/[name]-[hash].js",
},
},
},
});
Con nombres versionados por contenido, puedes servir cada chunk con cabeceras de caché inmutable y una vida larguísima, porque una URL distinta significa siempre contenido distinto. Cuando actualizas una dependencia, solo cambia el hash del vendor chunk; el navegador descarga ese y reutiliza todo lo demás. Es el matrimonio entre el chunking y el Cache-Control: immutable lo que convierte la separación en velocidad real para el usuario recurrente.
El beneficio se concentra en la segunda visita y en cada despliegue. Un usuario que vuelve tras un despliegue tuyo típico —cambiaste código de app, no dependencias— descarga solo el pequeño chunk de app y sirve todo el vendor desde su caché local: el arranque es casi instantáneo aunque tu app pese megabytes. Sin separación, ese mismo usuario redescargaría el bundle completo en cada despliegue. Por eso el vendor chunk no acelera la primera carga —esos bytes hay que traerlos igual— sino todas las siguientes, que en un producto activo son la inmensa mayoría de las sesiones.
Los hashes se propagan: si el chunk A importa al chunk B y cambia el hash de B, la referencia dentro de A también cambia, así que cambia el hash de A aunque su código sea idéntico. Un bundler bien diseñado mitiga esto con un mapa de importaciones o hashes deterministas, pero conviene saberlo: una actualización de una dependencia profunda puede invalidar más chunks de los que esperabas si la topología del grafo los encadena.
Para ver todo esto sin adivinar, los analizadores de bundle —integrados hoy en el flujo de Vite y Rolldown— dibujan un mapa de qué módulo acabó en qué chunk y cuánto pesa cada uno. Es la radiografía que convierte el chunking de caja negra en algo inspeccionable: si una dependencia aparece dos veces, la ves; si el vendor arrastra código de app que no debería, también.
Cuando un módulo duplicado mantiene estado —un store, un contexto de React, un singleton de configuración— tener dos copias no es solo desperdicio de bytes: son dos instancias que no se ven entre sí. Un componente lee de una y otro escribe en la otra, y brotan bugs desconcertantes de estado que no se propaga. Por eso la deduplicación en un chunk compartido no es solo optimización de tamaño; para el código con estado es una condición de corrección, y romperla a mano con manualChunks es introducir un fallo, no solo un sobrecoste.
La lección profunda de los chunks compartidos es que la mejor forma de partir un bundle no es por funcionalidad ni por carpeta, sino por ritmo de cambio y patrón de uso conjunto. Piénsalo como un problema de caché en el sentido más general: quieres agrupar en la misma unidad los bytes que cambian juntos y separar los que cambian a ritmos distintos, porque la unidad de invalidación es el chunk entero. Tu código de aplicación es volátil y se reescribe a diario; una librería estable es casi inmutable durante meses; el código compartido entre rutas se usa siempre pero se toca poco. Mezclar estas tres poblaciones en un mismo chunk es garantizar que la más volátil arrastre a las demás en cada despliegue, tirando a la basura una caché que podría haber durado semanas. Separarlas por su frecuencia de cambio hace que cada despliegue invalide solo lo mínimo, y que el usuario recurrente —que ya tiene el vendor y lo compartido en su caché— pague únicamente por lo que de verdad cambió. Esta idea conecta el code splitting con algo mucho mayor: es el mismo principio que gobierna el diseño de capas en una imagen de contenedor, la separación de datos calientes y fríos en un almacén, o la estructura de un lockfile frente al código que bloquea. En todos los casos la pregunta es idéntica —qué cambia junto y qué cambia por separado— y la respuesta define las fronteras de la unidad cacheable. Quien entiende el vendor chunk no ha aprendido un truco de bundler; ha reconocido, en un dominio concreto, una de las leyes más universales de los sistemas que quieren no repetir trabajo.
- Crea dos rutas divididas que importen la misma librería de terceros y confirma en el build que aparece un solo chunk compartido, no dos copias.
- Localiza en tu salida el chunk sin nombre de ruta y rastrea qué módulos contiene: verás que son los alcanzables desde varias entradas.
- Comprueba que tus chunks llevan un hash de contenido en el nombre y explica por qué eso habilita la caché inmutable.
- Cambia una línea de tu código de app, rehaz el build y verifica que el hash del vendor chunk no se movió.
- Actualiza ahora una dependencia y observa qué chunks cambian de hash; razona si la cascada afectó a más de los esperados.