Analizar el bundle: qué pesa de verdad
No puedes optimizar lo que no puedes ver. Tu build de producción emite una carpeta de chunks con nombres hasheados y una cifra total que dice poco. El primer acto de toda optimización seria no es tocar código, sino levantar un mapa: rollup-plugin-visualizer reparte cada byte del bundle entre los módulos que lo produjeron, y con ese treemap dejas de adivinar y empiezas a medir qué dependencia arrastra medio megabyte que nadie recuerda haber pedido.
Tu build escupe una carpeta de chunks —index-9f3a.js, vendor-a1b2.js, chunk-x7k.js— y una cifra total que no explica nada: 480 KB. ¿De quién son esos bytes? ¿Qué librería mete medio megabyte que nadie recuerda haber instalado? El primer acto de toda optimización seria no es tocar código, sino levantar un mapa. Un treemap reparte cada byte del bundle entre los módulos que lo produjeron, dibujando rectángulos cuya área es proporcional a su peso. Con rollup-plugin-visualizer dejas de intuir y empiezas a medir: la optimización sin perfilado previo es superstición.
- Instalar y configurar
rollup-plugin-visualizerpara emitir un treemap del build de producción. - Distinguir las tres tallas de un módulo —
stat,parsedygzip— y saber cuál importa al usuario. - Leer el treemap para localizar la dependencia que domina cada chunk.
- Reconocer los culpables recurrentes: imports totales, locales olvidados y duplicados de versión.
Del build opaco al treemap
El visualizer es un plugin que se engancha al final de la generación del bundle: cuando Rollup o Rolldown han producido los chunks, recorre el bundle en el hook generateBundle, mide cada módulo y emite un HTML interactivo. No cambia tu salida; solo la fotografía. Como conserva la compatibilidad con la API de plugins de Rollup, funciona igual bajo Vite 8 con Rolldown que bajo el Rollup clásico.
// vite.config.ts — el visualizer solo corre en el build, como ultimo plugin
import { defineConfig } from "vite";
import { visualizer } from "rollup-plugin-visualizer";
export default defineConfig({
build: {
rollupOptions: {
plugins: [
visualizer({
filename: "stats/treemap.html",
template: "treemap",
gzipSize: true,
brotliSize: true,
sourcemap: true,
open: true,
}),
],
},
},
});
Colocarlo dentro de build.rollupOptions.plugins garantiza que no se active en el dev server, donde no hay bundle que medir. La opción template elige la representación —treemap para peso por área, sunburst para jerarquía radial, network para el grafo de importaciones—; el treemap es el mejor punto de partida porque el ojo compara áreas mejor que ángulos. Activar sourcemap: true es clave: sin él, el visualizer atribuye bytes al chunk final; con él, usa los source maps para remontar cada byte hasta el módulo fuente que lo generó, incluso a través de la minificación.
Si solo quieres una foto puntual, npx vite-bundle-visualizer envuelve al plugin y genera el treemap sin editar tu vite.config.ts. Para bibliotecas o bundlers ajenos, source-map-explorer parte únicamente de los source maps del dist y funciona con cualquier herramienta que los emita. Distintas puertas, el mismo mapa.
Las tres tallas de un módulo
El treemap muestra tres cifras por módulo, y confundirlas lleva a optimizar el número equivocado. No son lo mismo, y solo una llega al usuario.
| Talla | Qué mide | Cuándo engaña |
|---|---|---|
stat |
El tamaño del módulo en el código fuente, antes de transformar | Ignora el tree-shaking y la minificación: infla lo que en realidad se descarta |
parsed |
El tamaño en el bundle final, ya minificado | Es lo que el motor de JS debe parsear, pero no lo que viaja por la red |
gzip / brotli |
El tamaño comprimido que sale del servidor | La única cifra que el usuario descarga de verdad |
La talla stat es la más grande y la más traicionera: una dependencia puede ocupar 300 KB de fuente y aportar 4 KB al bundle porque tu código solo usa una función y el tree-shaking tira el resto. Optimizar mirando stat te hace perseguir fantasmas. La talla parsed importa para el coste de CPU —parsear y compilar JavaScript no es gratis—, pero la que gobierna el tiempo de red, y por tanto el presupuesto que defenderás en la siguiente lección, es la comprimida: gzip o, mejor, brotli, que es lo que sirve cualquier CDN moderno.
Dos módulos de igual talla parsed pueden comprimir de forma muy distinta: el texto repetitivo —muchos nombres largos, mucha estructura similar— se aplasta bien, mientras que el código ya denso apenas cede. Por eso el ranking por brotli no coincide con el ranking por parsed, y por eso siempre ordenas por la cifra que el usuario paga, no por la que más impresiona.
Leer el mapa y cazar al culpable
Un treemap se lee como un mapa catastral: cada rectángulo grande es un módulo o paquete que ocupa terreno, y el anidamiento refleja la jerarquía de carpetas de node_modules. Buscas los bloques grandes que no esperabas, y casi siempre son el mismo puñado de sospechosos que se repite en cualquier producto.
Import total de una utilería
Traer todo lodash en vez de lodash-es con imports por método, o toda una librería de iconos por un solo icono. El árbol lo delata: un bloque enorme del que usas una esquina.
Locales e idiomas ocultos
Librerías de fechas o de internacionalización que arrastran decenas de locales que nunca cargas. Un clásico que hincha el vendor sin que nadie lo pida.
Duplicados de versión
La misma dependencia aparece dos veces bajo rutas distintas porque dos paquetes fijan rangos incompatibles. El treemap muestra dos bloques gemelos: dinero tirado y, si tiene estado, un bug.
Peso en el chunk inicial
Una dependencia pesada que solo usa una pantalla secundaria pero viaja en el bundle de arranque. No sobra: está en el sitio equivocado, y su cura es diferirla.
El flujo de trabajo es un bucle cerrado, no un acto único. Construyes, lees el mapa, identificas el módulo dominante, actúas —lo sustituyes por una alternativa ligera, lo difieres a un import() dinámico, o consolidas su versión duplicada— y vuelves a construir para confirmar que el rectángulo encogió. Cada iteración recorta el bloque más gordo que quede, siguiendo la ley de que el 20% de los módulos suele explicar el 80% del peso.
flowchart LR build[npm run build] --> stats[visualizer emite el treemap] stats --> leer[Localizas el modulo dominante] leer --> accion[Sustituyes difieres o deduplicas] accion --> build style stats fill:#89b4fa,color:#11111b style accion fill:#a6e3a1,color:#11111b
Antes de reescribir un import, comprueba en la talla parsed que la dependencia pesa de verdad en el bundle final. Un paquete con stat gigante puede aportar casi nada si tu uso es mínimo y la librería está bien marcada con sideEffects. Perseguir su stat es trabajo sin recompensa; el treemap existe precisamente para que ataques bytes reales, no bytes potenciales que el bundler ya descartó.
Hay una regla que separa al ingeniero de rendimiento del que reza: no toques una línea antes de haber medido. La intuición sobre qué pesa en un bundle es sistemáticamente falsa, y por una razón profunda —el grafo de módulos es un objeto de miles de nodos, con transformaciones, tree-shaking, compresión y deduplicación operando en cascada, y ningún cerebro humano simula ese pipeline de memoria—. Quien optimiza por corazonada acaba recortando lo que ya era barato mientras ignora el bloque de 300 KB que domina la talla comprimida, porque lo importa una dependencia transitiva que jamás miró. El treemap invierte la relación: primero conviertes lo invisible en una imagen con áreas proporcionales, y solo entonces decides, guiado por la evidencia, dónde invertir el esfuerzo. Esto no es una anécdota del frontend; es el mismo principio que gobierna todo el perfilado de sistemas, desde un flamegraph de CPU hasta un plan de ejecución de una base de datos: medir antes de actuar, atacar el cuello de botella real y no el imaginado, y volver a medir para confirmar que el cambio movió la aguja y no otra cosa. El corolario es igual de duro: una optimización sin una medición que la respalde no es una mejora, es una hipótesis sin verificar que a menudo empeora el código —lo vuelve más oscuro— sin tocar el número que importaba. Interiorizar el treemap no es aprender un plugin; es adoptar la disciplina de que en rendimiento la opinión no cuenta, solo cuenta la cifra que el usuario descarga.
- Añade
rollup-plugin-visualizercontemplate: "treemap",brotliSize: trueysourcemap: true, y genera el HTML de un build de producción real. - Ordena mentalmente los tres bloques más grandes por su talla
brotliy anota qué dependencia es cada uno. - Elige el bloque dominante y averigua quién lo importa: ¿tu código, o una dependencia transitiva que ni recordabas?
- Contrasta su talla
statcon su tallaparsed: si difieren mucho, entiende por qué el tree-shaking ya hizo parte del trabajo. - Aplica una acción —import por método, diferido o dedupe—, reconstruye y confirma sobre el mapa que el rectángulo encogió de verdad.