wandres.dev
RENDIMIENTO · Core Web Vitals

Analizar el bundle: chunks, dependencias y división

Leer el grafo de código que el build entrega: qué es un chunk y cómo Astro los agrupa, cómo visualizar el bundle en un treemap para descubrir dependencias pesadas escondidas, cómo trocear con dynamic import lo que solo hace falta a veces, y cómo aislar las librerías comunes en un chunk de vendor que se cachea entre páginas.

⏱ 15 min

Un build no entrega un archivo de JavaScript, entrega un grafo de trozos —los chunks— que el navegador descarga y ejecuta según los necesita. Analizar el bundle es aprender a leer ese grafo: cuántos trozos hay, cuánto pesa cada uno, qué esconden dentro y —lo más productivo— cómo redibujar sus fronteras para que coincidan con el uso real. Astro emite un bundle ya pequeño gracias a las islas, pero cuando una dependencia se cuela donde no debe, el arte de trocear es lo que separa un sitio ligero de uno que arrastra peso muerto.

🎯 Al terminar esta lección sabrás
  • Leer los chunks de la salida del build y entender qué los agrupa.
  • Visualizar el bundle en un treemap para hallar peso oculto.
  • Trocear código bajo demanda con import dinámico.
  • Aislar las dependencias comunes en un chunk de vendor.

Anatomía de la salida: qué es un chunk

Un chunk es una unidad de código que el bundler decide agrupar y emitir junta. Astro, sobre Vite 8 y Rolldown, produce varios tipos: el runtime compartido de un framework, el código propio de cada isla, y los trozos que resultan de un import dinámico. Cada archivo lleva un hash de contenido en el nombre, y ese detalle no es cosmético: es lo que permite cachearlo para siempre, porque si el contenido cambia, cambia el nombre.

npm run build
# dist/_astro/ agrupa los chunks con hash de contenido
# client.9f2a1c.js     3.7 kB gzip   runtime compartido
# Comentarios.4b8e0d.js 14.8 kB gzip  isla concreta
# vendor.c1d7a9.js     38.2 kB gzip   librerias comunes

La clave conceptual es que el bundler intenta que el código compartido por varias páginas viva en un chunk común, para descargarlo una vez y reutilizarlo. Cuando dos páginas usan la misma isla, su código no se duplica: se emite en un trozo que ambas referencian. Entender esa lógica de agrupación es el primer paso para influir en ella.

📝
Menos peticiones importa menos que antes, pero no cero

Con HTTP/2 y HTTP/3, muchas descargas viajan multiplexadas por una sola conexión, así que el viejo miedo a demasiadas peticiones se ha suavizado: partir en más chunks ya no penaliza como en la era de HTTP/1. Pero no desaparece del todo. Cada chunk sigue teniendo su coste de descubrimiento, de cabeceras y de ejecución por separado, y una cascada de cientos de trozos diminutos aún se nota en la carga. La granularidad ideal no es un solo archivo gigante ni mil esquirlas: es la que agrupa lo que cambia y se usa junto.

Visualizar: el treemap no miente

La salida de texto dice cuánto pesa cada chunk, pero no qué lo infla. Para eso está el mapa de áreas, donde cada módulo es un rectángulo cuya superficie es proporcional a su peso. Un vistazo basta para que el culpable salte: casi siempre es un rectángulo enorme con el nombre de una dependencia que no esperabas tan cara.

npx vite-bundle-visualizer
# abre un treemap navegable del bundle completo
💡
Busca el rectángulo que no reconoces

La lectura productiva de un treemap no empieza por tu código, sino por lo que no escribiste tú. Un formateador de fechas, una librería de iconos importada entera, un cliente de red pesado: esas piezas suelen ocupar más que toda tu lógica junta. Cuando identifiques una, pregúntate tres cosas en orden: ¿la necesito de verdad, puedo importar solo la parte que uso, o existe una alternativa nativa o más ligera? El treemap no propone soluciones, pero señala sin piedad dónde buscarlas.

import dinámico: cargar cuando hace falta

No todo el código de una isla se usa siempre. Un editor de texto enriquecido, un selector de emojis, una librería de gráficos: cosas caras que solo entran en juego tras una acción del usuario. El import dinámico —import() como función que devuelve una promesa— le dice al bundler que separe ese código en su propio chunk y lo descargue solo cuando se invoque.

export default function Nota() {
  async function abrirEditor() {
    // El editor pesado no viaja en la carga inicial:
    // se descarga en su propio chunk al pulsar
    const { montarEditor } = await import('./editor-pesado');
    montarEditor();
  }
  return <button onClick={abrirEditor}>Editar</button>;
}

El efecto sobre el presupuesto es directo: la isla se hidrata con un botón diminuto, y el peso grande solo llega si el visitante lo pide. Es la misma filosofía de las directivas perezosas —el coste que no se paga— llevada al interior de una isla, a la granularidad de una función.

Dividir el vendor con manualChunks

Por defecto el bundler decide la agrupación, pero a veces conviene guiarlo. Aislar las librerías de terceros —el vendor— en su propio chunk estable tiene una ventaja de caché: mientras tu código cambia en cada despliegue y renueva su hash, el vendor permanece idéntico y el navegador lo reutiliza de una visita a otra sin volver a descargarlo.

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  vite: {
    build: {
      rollupOptions: {
        output: {
          manualChunks: {
            vendor: ['react', 'react-dom'],
          },
        },
      },
    },
  },
});
⚠️
Dividir mal empeora, no mejora

Trocear no es gratis ni siempre bueno. Demasiados chunks diminutos multiplican las peticiones y añaden latencia; un vendor gigantesco fuerza a descargar librerías que una página concreta no usa. La división rinde cuando la frontera del chunk coincide con la frontera del uso: código que cambia junto y se usa junto, en un mismo trozo. Mide antes y después de tocar manualChunks; si el peso total o el número de peticiones empeora, deshaz el cambio. Rolldown, además, ofrece un control más fino con su chunking avanzado cuando manualChunks se queda corto.

Medir el coste antes de instalar

El mejor momento para analizar una dependencia no es después de que engorde el bundle, sino antes de añadirla. Cada npm install es una decisión de rendimiento disfrazada de comodidad, y conviene tomarla con el dato delante: cuánto pesa la librería una vez empaquetada, si permite importar solo una parte, y si arrastra dependencias propias.

Servicios como Bundlephobia estiman el peso gzip de un paquete y su efecto antes de instalarlo, y el propio treemap confirma después la realidad. La regla sana es tratar una dependencia de cliente como un cargo permanente: entra en el presupuesto y se paga en cada visita, así que su valor debe justificar su peso.

  • ¿Se puede importar solo la función que uso, o la librería es indivisible?
  • ¿Existe una alternativa nativa del navegador que evite la dependencia entera?
  • ¿Vive este trabajo mejor en el servidor, donde no cuesta nada al visitante?

Preguntarlo antes de instalar es más barato que arrepentirse después de desplegar. El análisis del bundle no es solo una autopsia que se hace al final; es también una prueba previa que evita el peso muerto antes de que exista.

🧩

El chunk como unidad

Runtime, isla o import dinámico: cada trozo se descarga y cachea por separado, con hash de contenido.

🗺️

El treemap acusa

El área de cada rectángulo es su peso. La dependencia inesperada salta a la vista de inmediato.

import dinámico

Separa el código caro en su chunk y lo descarga solo cuando una acción lo invoca.

📦

vendor estable

Aislar las librerías comunes las cachea entre páginas y despliegues, si la frontera coincide con el uso.

flowchart TB
ENTRY[entrada de la pagina] --> APP[codigo de tus islas]
ENTRY --> VENDOR[vendor react y libs comunes]
APP -.import dinamico.-> LAZY[chunk bajo demanda]
VENDOR --> CACHE[se reutiliza entre paginas y despliegues]
LAZY --> DEM[llega solo si el usuario lo pide]
style VENDOR fill:#cba6f7,color:#11111b
style LAZY fill:#89b4fa,color:#11111b
style CACHE fill:#a6e3a1,color:#11111b
style DEM fill:#a6e3a1,color:#11111b
Empaquetar es particionar un grafo, y el arte es alinear las fronteras con el uso

Debajo de la palabra bundle hay un problema matemático viejo y elegante: dado el grafo de módulos de tu aplicación —quién importa a quién—, ¿cómo lo cortas en trozos que se descarguen y se cacheen de la mejor manera posible? No hay una respuesta única, porque hay tensiones que tirar de un lado afloja del otro. Menos chunks significa menos peticiones pero peor reutilización, porque un cambio pequeño invalida un trozo grande. Más chunks mejora la caché pero multiplica el ida y vuelta con el servidor. Aislar el vendor gana estabilidad pero puede enviar librerías que una página no toca. El bundler resuelve este problema con heurísticas buenas por defecto, y por eso la mayoría de las veces no debes tocar nada: Rolldown parte el grafo con criterio. Pero cuando lo tocas, el principio que debe guiarte es uno solo, y es hermoso por lo general que resulta: la frontera de un chunk debe coincidir con una frontera de uso. Código que cambia junto pertenece al mismo trozo, para que un despliegue invalide lo mínimo. Código que se usa junto pertenece al mismo trozo, para que una página descargue lo que necesita y nada más. Código que solo hace falta a veces merece su propio trozo diferido, para que la carga inicial no cargue con lo eventual. Cuando entiendes el empaquetado así —no como una caja negra que produce archivos, sino como el acto de particionar un grafo según cómo el código cambia y se usa—, dejas de pelear con el bundler y empiezas a colaborar con él. Analizar el bundle no es una tarea de auditoría que se hace al final; es la forma de ver el grafo que siempre estuvo ahí, y de decidir con conocimiento dónde trazar los cortes.

⚔️ Redibuja las fronteras de tu bundle
  1. Ejecuta npm run build y clasifica los chunks de dist/_astro/ en runtime, islas y trozos dinámicos según su nombre y tamaño.
  2. Abre un treemap con vite-bundle-visualizer y localiza la dependencia que más pesa dentro de tu isla más grande.
  3. Envuelve la carga de esa dependencia en un import dinámico disparado por una acción del usuario y comprueba en la red que ya no viaja en la carga inicial.
  4. Aísla tus librerías comunes en un chunk de vendor con manualChunks, reconstruye y confirma que el peso total no empeora.