wandres.dev
JAVASCRIPT II · Presupuesto y análisis del bundle

Analizar el bundle: leer el mapa de tamaños sin engañarte

Cómo se generan los visualizadores de bundle, qué tres tamaños distintos muestran y cuál es el que importa, por qué el gráfico de rectángulos engaña, y las preguntas concretas que hay que hacerle al informe.

⏱ 18 min

El visualizador de bundle es la herramienta más usada y peor leída del rendimiento frontend. Produce una imagen bonita de rectángulos anidados que todo el mundo entiende a primera vista y casi nadie interpreta bien, porque muestra tres magnitudes distintas bajo el mismo dibujo y la que se ve por defecto no suele ser la que importa. Saber leerlo consiste en hacerle cuatro preguntas concretas en lugar de mirarlo esperando que algo salte a la vista.

🎯 Al terminar esta lección sabrás
  • Explicar de dónde salen los datos del visualizador y qué precisión tienen.
  • Distinguir los tres tamaños que se muestran y elegir el correcto para cada decisión.
  • Reconocer las tres formas en que el gráfico de rectángulos induce a error.
  • Hacerle al informe las cuatro preguntas que producen acciones.

De dónde salen los datos

Todos los visualizadores serios funcionan igual: leen el mapa de fuentes del bundle generado. El mapa de fuentes es un fichero que dice, para cada tramo del código de salida, de qué fichero de entrada y de qué posición procede. Sumando la longitud de todos los tramos que apuntan a un mismo módulo se obtiene cuánto de la salida procede de ese módulo.

Esto tiene tres consecuencias que hay que tener presentes al leer cualquier informe.

La precisión depende de la calidad del mapa. Si una parte de tu cadena de compilación pierde o degrada el mapa —un plugin que transforma código sin actualizar posiciones, una minificación con mapas desactivados— aparecerán tramos sin atribuir, típicamente etiquetados como desconocidos. Un informe con un 15 por ciento sin atribuir no es utilizable.

El código generado por el empaquetador no pertenece a nadie. El tiempo de ejecución del propio empaquetador, los envoltorios de módulo, los ayudantes del transpilador: aparecen como una entrada aparte o se reparten de forma arbitraria. En bundles pequeños puede ser el 10 o el 15 por ciento del total, y es real aunque no sea tuyo.

Necesitas mapas de fuentes de la compilación de producción, no de la de desarrollo. Analizar el bundle de desarrollo es analizar otro programa: sin minificar, sin eliminación de código muerto, con comprobaciones y avisos que no llegan a producción. Es un error frecuente y produce conclusiones invertidas.

La configuración mínima que genera el informe en las tres herramientas más habituales:

// vite.config.js  — el visualizador funciona sobre la salida de Rollup
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  build: { sourcemap: true },
  plugins: [
    visualizer({
      filename: 'stats.html',
      gzipSize: true,
      brotliSize: true,
      template: 'treemap',   // 'sunburst' y 'network' tambien existen
    }),
  ],
});
# esbuild: genera un metafichero y subelo al analizador oficial
esbuild src/main.ts --bundle --minify --metafile=meta.json --outfile=dist/app.js

# webpack: genera estadisticas para el analizador
webpack --profile --json > stats.json

Los tres tamaños, y cuál usar

El mismo módulo aparece con tres cifras muy distintas, y elegir la equivocada lleva a optimizar lo que no toca.

Tamaño original. Lo que ocupa el módulo antes de minificar. Es la cifra más grande y la menos útil: no corresponde a nada que llegue al usuario. Sirve para una sola cosa, comparar la verbosidad relativa de dos módulos.

Tamaño minificado. Lo que ocupa en el fichero de salida. Es el número que corresponde al trabajo de parseo y compilación que hará el motor, porque es el texto que va a leer. Es la cifra correcta cuando tu problema es el tiempo de bloqueo del hilo principal.

Tamaño comprimido con gzip o Brotli. Lo que viaja por la red. Es el número correcto cuando tu problema es la descarga, y es el que hay que usar contra un presupuesto expresado en bytes transferidos.

La diferencia entre las dos últimas no es un factor constante entre módulos, y ahí está el detalle que engaña. Un módulo con muchas cadenas de texto repetidas —tablas de traducción, datos de zonas horarias, listas de países— comprime extraordinariamente bien: puede pasar de 180 KB minificados a 22 KB con Brotli, un factor de 8. Un módulo de lógica densa comprime con un factor de 3.

La consecuencia práctica: en la vista comprimida, los módulos de datos parecen inofensivos y no lo son. Esos 180 KB minificados hay que parsearlos igual, y en un móvil de gama media son 200 milisegundos de hilo principal por 22 KB de red. Si miras solo la vista comprimida, ese módulo no aparece en tu radar.

La regla: mira las dos vistas, siempre. La comprimida para decidir qué recortar por red, la minificada para decidir qué recortar por CPU. Cuando discrepan, la minificada suele señalar el problema más caro.

Las tres formas en que el gráfico engaña

El área es proporcional al tamaño, y tu ojo no percibe áreas. Un rectángulo que parece «el doble» suele ser cuatro veces más grande. Nunca estimes proporciones a ojo: pasa el cursor y lee la cifra. El gráfico sirve para localizar, no para cuantificar.

Los módulos pequeños desaparecen. Doscientos módulos de 1 KB suman 200 KB y son invisibles en el gráfico, repartidos en rectángulos de un píxel. Esta es la forma más común de que un bundle engorde sin que nadie lo vea: no por una dependencia gorda, sino por doscientas pequeñas. La detección exige la vista de tabla ordenada, no la visual.

Un módulo puede aparecer varias veces. Si dos dependencias tuyas usan versiones incompatibles de una tercera, el gestor de paquetes instala las dos y el empaquetador incluye las dos. En el gráfico aparecen en ramas distintas del árbol y no saltan a la vista. Es una de las causas más frecuentes de bundles inexplicablemente grandes.

# Duplicados: lo primero que hay que mirar en un bundle sospechoso
npm ls --all react
npm ls --all lodash

# O directamente sobre el arbol instalado
npm dedupe --dry-run

Las cuatro preguntas

En lugar de mirar el informe esperando inspiración, hazle estas cuatro preguntas por orden. Producen acciones concretas.

Uno: ¿cuál es el módulo de terceros más grande, y qué hace exactamente? Casi siempre hay uno que ocupa entre el 15 y el 40 por ciento del total. La pregunta correcta no es si es grande sino si todo él es necesario en la ruta crítica. Esto tiene su propia lección: encontrar la dependencia gorda.

Dos: ¿hay algo aquí que no debería estar en absoluto? Datos de idiomas que no ofreces, iconos que no usas, un componente de gráficas en la página de inicio, código de pruebas, mensajes de desarrollo. Este barrido produce recortes grandes con riesgo cero y es lo primero que hay que hacer.

Tres: ¿cuánto pesa lo que solo se usa después de una interacción? Modales, editores, selectores de fecha, mapas, reproductores. Todo eso puede salir del arranque sin quitarle una funcionalidad a nadie. Es el material del troceado por interacción.

Cuatro: ¿qué proporción del bundle es tuya y cuál es de dependencias? La cifra típica en aplicaciones con marco está entre el 70 y el 85 por ciento de dependencias. Si tu código propio es el 60 por ciento, el trabajo está en tu código; si es el 15, no hay reescritura tuya que arregle nada y el trabajo está en la lista de dependencias.

El visualizador te dice qué hay en el bundle, no qué se ejecuta; el panel de cobertura sí

Aquí está la limitación de fondo de todos los visualizadores, y la razón de que un análisis basado solo en ellos se estanque: el mapa de tamaños describe el fichero, no la ejecución. Un módulo que ocupa 60 KB y del que solo se ejecutan tres funciones aparece exactamente igual que uno que se ejecuta entero.

La herramienta complementaria es el panel de cobertura del navegador, que instrumenta el código y marca qué bytes se han ejecutado de verdad durante una sesión. La forma correcta de usarlo es esta: abre el panel, empieza a grabar, carga la página, haz el recorrido completo del usuario típico, y para. Lo que quede sin cubrir es código que enviaste y nadie ejecutó.

Los números que suele dar sorprenden: en una aplicación con marco es habitual encontrar entre un 40 y un 65 por ciento de JavaScript sin ejecutar tras una sesión completa. Esa cifra es la mejor estimación disponible de cuánto margen hay.

Tres advertencias sobre cómo interpretarla, porque el panel también engaña si se lee ingenuamente.

No todo el código sin cubrir es eliminable. Los manejadores de error, las ramas de compatibilidad y el código de rutas alternativas están ahí para cuando hagan falta. La cobertura te dice dónde mirar, no qué borrar.

La granularidad es por línea, no por función. Una función de la que se ejecuta la primera línea y se sale por un return temprano cuenta como parcialmente cubierta, y el visor lo pinta a franjas. En módulos con muchas guardas, la cifra global se distorsiona.

Y la trampa gorda: el código sin cubrir que sí se ejecutará más tarde no distingue del que no se ejecutará nunca. La forma de separarlos es hacer dos grabaciones, una del arranque y otra de la sesión completa, y comparar. Lo que aparece cubierto en la segunda y no en la primera es exactamente el candidato ideal a carga diferida: código necesario, pero no ahora. Ese conjunto es el que produce las mejores ganancias de arranque de todo este nivel, y solo se ve cruzando las dos grabaciones.

Un apunte final que ahorra confusión: el panel de cobertura instrumenta y por tanto ralentiza la página. Los tiempos que veas mientras grabas cobertura no son válidos para nada. Cobertura y perfil de rendimiento son dos sesiones distintas.

Convertir el análisis en seguimiento

Un análisis puntual sirve una vez. Lo que evita la reincidencia es tener la cifra en cada cambio, y para eso basta con guardar el tamaño por ruta en un fichero versionado y compararlo.

// scripts/tamanos.mjs — ejecutar tras la compilacion
import { readdir, readFile, stat } from 'node:fs/promises';
import { gzipSync, brotliCompressSync } from 'node:zlib';
import { join } from 'node:path';

const dir = 'dist/assets';
const filas = [];
for (const f of await readdir(dir)) {
  if (!f.endsWith('.js')) continue;
  const buf = await readFile(join(dir, f));
  filas.push({
    fichero: f,
    minificado: buf.length,
    gzip: gzipSync(buf).length,
    brotli: brotliCompressSync(buf).length,
  });
}
filas.sort((a, b) => b.brotli - a.brotli);
console.table(filas);
console.log('TOTAL brotli KB:', (filas.reduce((s, r) => s + r.brotli, 0) / 1024).toFixed(1));

Con eso en la salida de cada compilación y el total guardado por cambio, tienes la serie temporal que permite la conversación de la que hablaba el presupuesto: no «esta biblioteca son 42 KB», sino «hemos crecido 180 KB en cuatro meses y aquí está el desglose por incorporación».

⚔️ Reto práctico

Genera el informe de tu bundle de producción y responde por escrito a las cuatro preguntas, con cifras. Después ejecuta el panel de cobertura dos veces, una solo con la carga y otra con el recorrido completo, y anota los tres módulos con más bytes cubiertos en la segunda y no en la primera. Esos tres son tu lista de trabajo para el nivel de troceado.