wandres.dev
TREE SHAKING · eliminar código muerto

Verificar y depurar el tree shaking: por qué un módulo no se fue

El tree shaking es invisible hasta que lo inspeccionas, y su fallo más común no es que pode de menos por azar, sino por una causa concreta que se puede diagnosticar: un sideEffects ausente, una interop CommonJS, un namespace pasado entero, un efecto real que obliga a conservar. Cómo leer el bundle con un visualizador y con el metafile, cómo buscar un símbolo que debería haber desaparecido, cómo usar las opciones de treeshake de Rollup para afinar el análisis, y por qué medir en dev no cuenta.

⏱ 16 min

El tree shaking es una optimización invisible: ocurre en silencio y su resultado es una ausencia, lo más difícil de observar. Por eso la habilidad que cierra este nivel no es activarlo —ya está activo— sino verificarlo: abrir el bundle, buscar el símbolo que esperabas ver desaparecer, y cuando sigue ahí, diagnosticar por qué. Y casi nunca es por azar. Un módulo que “no se fue” arrastra una causa concreta y reconocible: un sideEffects que falta, una interop CommonJS, un namespace pasado entero, un efecto que de verdad había que conservar. Depurar el tree shaking es aprender a leer esas firmas en el artefacto y en el grafo.

🎯 Al terminar esta lección sabrás
  • Inspeccionar el bundle con visualizadores y con el metafile para ver qué sobrevivió.
  • Buscar un símbolo concreto en el dist y confirmar si se podó o no.
  • Diagnosticar las causas típicas de que un módulo no se elimine.
  • Afinar el análisis con las opciones de treeshake de Rollup y medir siempre en producción.

Hacer visible lo invisible

No puedes optimizar lo que no mides, y el tree shaking exige mirar el artefacto final, no el código fuente. La primera herramienta es un visualizador del bundle: rollup-plugin-visualizer o vite-bundle-visualizer producen un mapa de árbol donde el tamaño de cada bloque es proporcional a lo que ese módulo aporta al bundle. Un vistazo te dice qué dependencia pesa de más y, a menudo, cuál se coló entera cuando esperabas una parte.

Antes que cualquier herramienta, sin embargo, hace falta una disposición mental: tratar el bundle como un objeto que se inspecciona, no como una caja negra que se acepta. La mayoría de los problemas de tamaño sobreviven no porque sean difíciles de diagnosticar, sino porque nadie llegó a abrir el artefacto y mirar. La primera victoria del tree shaking maduro es puramente cultural —hacer de la inspección un hábito— y solo después vienen las herramientas que la vuelven cómoda.

La segunda es más quirúrgica: el metafile. esbuild —y Rolldown, que lo hereda— puede emitir un JSON con exactamente qué entró en cada chunk y por qué. Es la fuente de la verdad para responder “¿de dónde salió este módulo?”.

La palabra verdad no es un adorno. El visualizador te muestra el resultado —qué pesa—, pero el metafile te muestra el razonamiento —qué importó a qué—, y en un diagnóstico esa cadena causal es justo lo que necesitas. Un módulo grande no te confiesa cómo llegó; el metafile sí, y remontar sus aristas desde el sospechoso hasta un punto de entrada es la forma más directa de dar con el import concreto que lo introdujo.

# Visualizador: un treemap del bundle abierto en el navegador
npx vite-bundle-visualizer

# Metafile de esbuild: el detalle de que entro y de donde
esbuild app.js --bundle --metafile=meta.json --outfile=out.js

La diferencia entre las dos herramientas es de propósito. El visualizador es para explorar: lo abres cuando aún no sabes qué pesa de más y quieres una panorámica. El metafile es para interrogar: lo consultas cuando ya tienes una hipótesis concreta —“este módulo no debería estar aquí”— y necesitas la cadena exacta de imports que lo trajo. Uno responde a la pregunta “¿qué es grande?”; el otro, a “¿por qué está esto?”. Un buen diagnóstico suele empezar con el primero y terminar con el segundo.

Buscar el símbolo que debía irse

La comprobación más directa no necesita herramientas sofisticadas: grep sobre el dist. Si esperabas que resta se podara, búscala en la salida. Si no aparece, el tree shaking hizo su trabajo; si aparece, tienes un caso que diagnosticar. Es tosco pero infalible como test de presencia, y conviene automatizarlo cuando el tamaño importa.

Su virtud es la ausencia de ambigüedad. Un visualizador te da tamaños que hay que interpretar; un grep te da un sí o un no. Cuando la pregunta es binaria —“¿sobrevivió este símbolo concreto?”— una respuesta binaria es exactamente lo que quieres, sin gráficos que leer ni umbrales que juzgar. Por eso es el primer instrumento del depurador, aunque no el más vistoso de la caja.

# Construye para produccion y busca un simbolo que deberia haber desaparecido
vite build
grep -r "resta" dist/            # sin resultados = podado; con resultados = sobrevivio

El grep sobre dist tiene un matiz que conviene conocer: la minificación renombra los símbolos locales, así que buscar un nombre interno puede no encontrarlo aunque el código siga ahí, solo que rebautizado con una letra. Para un export público el nombre suele conservarse y la búsqueda es fiable; para un identificador interno, conviene construir sin minificar —o apoyarse en los sourcemaps— antes de buscar. Aun con esa salvedad, es la comprobación más rápida que existe y la primera que deberías hacer cuando dudes de si algo se fue.

⚠️
Mide en producción o no estás midiendo nada

El error número uno al evaluar el tree shaking es hacerlo en desarrollo. En vite dev, Vite sirve ESM sin empaquetar y no hay grafo cerrado que sacudir ni minificador que pase: todo tu código y el de tus dependencias parece estar presente. Eso no es un fallo de poda, es que la poda aún no ha ocurrido. Cualquier conclusión sobre qué se elimina debe salir de un vite build de producción, sobre el contenido de dist. Si tu métrica sale de la pestaña de red del dev server, estás midiendo un artefacto que nunca llegará al usuario.

📝
Un presupuesto de tamaño convierte la vigilancia en automática

Verificar a mano está bien para depurar, pero para que una regresión de tamaño no se cuele conviene automatizar la comprobación. Las herramientas de bundle budget fallan el build cuando un chunk supera un umbral, y un test puede afirmar que cierto símbolo no aparece en dist. Así, el día que alguien rompe el tree shaking sin querer —un import de más, una dependencia CommonJS recién añadida— el pipeline lo detiene en vez de dejar que el peso muerto llegue a producción y lo descubra un usuario con conexión lenta.

Por qué un módulo “no se fue”

Cuando un símbolo sobrevive contra tu expectativa, el diagnóstico casi siempre cae en una lista corta de causas conocidas. Recorrerlas en orden resuelve la inmensa mayoría de los casos.

  • Falta sideEffects. La dependencia no declaró sideEffects: false, así que el bundler conserva sus módulos por prudencia aunque no uses sus exports.
  • Es CommonJS. Importas una librería que por dentro es CJS; su envoltorio de interop es opaco al análisis y se arrastra entera.
  • Un namespace dinámico. Un import * as ns que se pasa completo a una función o se accede con clave calculada apaga la poda de ese módulo.
  • Un efecto real. El módulo tiene un efecto secundario legítimo —un registro, un polyfill— y conservarlo es lo correcto, no un fallo.
  • Se usa de verdad. El símbolo se alcanza por un camino del grafo que no habías visto: un reexport transitivo, un import indirecto.
💡
Recorre las causas en orden, de la más común a la más rara

Ante un módulo que sobrevive, resistir la tentación de tocar la config y seguir la lista en orden ahorra horas. Primero: ¿la dependencia declara sideEffects? Es, con diferencia, la causa más frecuente. Después: ¿es CommonJS? La segunda más frecuente. Solo si ambas se descartan tiene sentido sospechar de un namespace dinámico, de un efecto real o de un uso transitivo que no habías rastreado. El error habitual es empezar por el final, trasteando con las opciones de treeshake, cuando la causa casi siempre vive en las dos primeras preguntas.

Fíjate en que cuatro de las cinco causas no son bugs en absoluto. Que un módulo CommonJS se arrastre entero, que un import de efecto se conserve, que un símbolo usado transitivamente sobreviva: todo eso es el bundler comportándose correctamente dado lo que ve. La única entrada de la lista que es un fallo genuino —un sideEffects ausente o mal declarado— vive en la dependencia, no en tu herramienta. Depurar el tree shaking es, la mayoría de las veces, descubrir que el sistema tenía razón y que era tu expectativa la que estaba mal calibrada.

🔍

Visualizador

Un treemap donde el tamaño es el peso real en el bundle. Revela de un vistazo qué dependencia entró de más.

🧾

Metafile

El JSON de esbuild o Rolldown con qué entró en cada chunk y por qué. La fuente de la verdad del análisis.

⌨️

grep en dist

El test de presencia más simple: busca el símbolo en la salida de producción. Aparece o no aparece, sin ambigüedad.

🩺

La lista corta

sideEffects, CJS, namespace dinámico, efecto real, uso transitivo. Cinco causas cubren casi todo.

flowchart TD
A[el simbolo sobrevive en dist] --> B{la dependencia declara sideEffects}
B -->|no| S1[falta la promesa: se conserva]
B -->|si| C{es ESM o CommonJS}
C -->|CommonJS| S2[interop opaca: se arrastra entera]
C -->|ESM| D{se importa de forma estatica}
D -->|namespace dinamico| S3[analisis apagado]
D -->|named estatico| E[revisa efecto real o uso transitivo]
style S1 fill:#f38ba8,color:#11111b
style S2 fill:#f38ba8,color:#11111b
style S3 fill:#f38ba8,color:#11111b
style E fill:#f9e2af,color:#11111b

Afinar el análisis

Cuando has descartado las causas anteriores y sospechas que el bundler es más conservador de lo necesario, Rollup expone opciones de treeshake para calibrar su agresividad. No son para el día a día, pero son la herramienta correcta para diagnosticar y, con cuidado, para exprimir un caso concreto.

Conviene subrayar ese “con cuidado”. Estas opciones no hacen tu código más podable; hacen al bundler más crédulo. Relajar moduleSideEffects o propertyReadSideEffects no elimina efectos, solo le pide al análisis que asuma que no los hay, y si esa asunción es falsa reintroduces por config el mismo tipo de bug que un sideEffects mentiroso o un /* @__PURE__ */ falso. Son un bisturí para diagnosticar, no un acelerador para dejar puesto sin pensarlo.

// rollup / vite: control fino del analisis
export default {
  treeshake: {
    preset: 'recommended',        // 'safest' | 'recommended' | 'smallest'
    moduleSideEffects: true,      // asumir que los modulos pueden tener efectos
    propertyReadSideEffects: true, // asumir que leer una propiedad puede tener efectos
    unknownGlobalSideEffects: true // asumir que un global desconocido puede tenerlos
  }
}

Cada opción es una perilla entre corrección y tamaño. propertyReadSideEffects: false le dice al bundler que leer una propiedad nunca tiene efectos —cierto casi siempre, peligroso si usas getters con efectos—. El preset 'smallest' afloja varias a la vez para podar más, a cambio de asumir un código mejor comportado. La actitud correcta es diagnóstica: usa estas opciones para entender por qué algo se conserva, y ajústalas en producción solo cuando comprendas exactamente qué garantía estás cediendo.

Un uso astuto de estas perillas es diferencial. Construye una vez con los ajustes por defecto y otra con 'smallest' o con una sola opción relajada, y compara qué desaparece entre ambas. Lo que se va con la versión agresiva y no con la prudente es, precisamente, aquello que el análisis conservaba por una suposición que tú acabas de relajar. Esa comparación te dice no solo cuánto ganas, sino qué estabas conservando y por qué, que es la información que de verdad necesitas para decidir si el riesgo compensa.

En la inmensa mayoría de los proyectos, sin embargo, nunca tocarás estas opciones, y eso es una buena señal. Los valores por defecto están calibrados para el equilibrio correcto entre tamaño y seguridad, y la necesidad de moverlos suele delatar un problema aguas arriba —una dependencia mal empaquetada— que se arregla mejor en su origen que compensándolo con una config agresiva. Trátalos como un termómetro de diagnóstico antes que como una palanca de optimización permanente.

ℹ️
Rolldown y Oxc heredan el contrato, no lo reinventan

En Vite 8, quien sacude el árbol es Rolldown sobre el analizador de Oxc, pero el modelo mental que has construido no cambia: sigue mirando sideEffects, sigue honrando /* @__PURE__ */, sigue siendo conservador ante lo dinámico. Lo que gana es velocidad —el análisis corre en Rust— y coherencia, porque dev y build comparten motor. Al depurar, el metafile y los visualizadores siguen siendo tu instrumental; lo que cambia por debajo es el corazón, no el contrato que aprendiste.

La poda que no verificas es una fe, no un resultado

Cierra este nivel una idea que vale para toda optimización invisible: aquello que no mides, no lo controlas; solo lo crees. El tree shaking es especialmente traicionero en esto porque su producto es una ausencia, y las ausencias no saltan a la vista como saltan los errores. Un bundle puede arrastrar la mitad de una librería que creías podada durante meses, sin un solo síntoma, hasta que alguien abre el visualizador y descubre el peso muerto. Por eso la madurez con esta técnica no consiste en confiar en que “el bundler ya se encarga”, sino en convertir la verificación en un reflejo: construir para producción, abrir el artefacto, buscar el símbolo, leer el metafile. Y cuando algo no se fue, la actitud correcta no es encogerse de hombros ni desactivar la optimización, sino diagnosticar, porque —y esta es la lección que recorre las cinco lecciones— el tree shaking casi nunca falla por azar. Falla por una causa que ya conoces: una promesa de sideEffects que faltaba, una frontera con CommonJS que volvió opaco el grafo, un namespace que pasaste entero y apagó el análisis, un efecto real que el bundler tuvo razón en conservar, o un uso transitivo que no habías rastreado. Cada una de esas firmas remite a un principio de las lecciones anteriores: la poda vive de la estructura estática, se apoya en contratos declarados, y se detiene con prudencia ante lo que no puede probar. Depurar el tree shaking es, en el fondo, recorrer ese razonamiento hacia atrás desde el síntoma —un módulo que sobra— hasta la causa —el punto donde el grafo dejó de ser analizable o el contrato dejó de cumplirse—. Quien interioriza esto deja de vivir el tamaño de sus bundles como un misterio meteorológico y empieza a tratarlo como lo que es: un sistema legible, con reglas conocidas, cuyas sorpresas siempre tienen explicación. El objetivo último de este nivel no era que supieras que existe el tree shaking, sino que, ante un bundle inflado, sepas exactamente qué mirar, en qué orden, y por qué. Esa capacidad —ver la ausencia, nombrar su causa, corregirla— es la diferencia entre esperar que la herramienta te salve y saber conducirla.

⚔️ Conviértete en depurador del bundle
  1. Ejecuta un vite build con un visualizador y localiza la dependencia que más pesa; forma una hipótesis de por qué entró de ese tamaño.
  2. Elige un símbolo que creas podado y confírmalo con grep sobre dist; si sobrevive, recorre la lista corta de causas hasta dar con la suya.
  3. Genera un metafile con esbuild o Rolldown y rastrea de qué import concreto salió un módulo que no esperabas.
  4. Reproduce el error de medir en dev: comprueba que un símbolo “muerto” está presente en el dev server y ausente en dist.
  5. Ajusta una opción de treeshake —por ejemplo propertyReadSideEffects— y observa qué cambia y qué garantía cedes al hacerlo.