Tree Shaking con Claude Code: guía práctica para reducir bundles
Mejora el tree shaking con Claude Code: ESM, sideEffects, medición, errores comunes y ejemplos ejecutables.
Qué significa tree shaking en la práctica
Tree shaking es la optimización que elimina exports de JavaScript o TypeScript que no se usan en el bundle de producción. En términos simples, evita enviar al navegador código que la pantalla actual no necesita. No cambia tu producto por sí solo, pero reduce el coste de descarga, parseo y ejecución.
El bundler no conoce tus intenciones.
Decide con la información que puede analizar: import, export, el campo sideEffects de package.json, transformaciones a CommonJS y código que se ejecuta en el nivel superior del módulo.
Por eso aparecen fallos como “la función no usada sigue en el bundle” o “al poner sideEffects: false desapareció el CSS”.
Claude Code ayuda cuando le das una tarea medible. Primero debe registrar el tamaño actual, localizar dependencias CommonJS, revisar barrel files, separar exports y comprobar el resultado con un build de producción. Este artículo resume el flujo que Masa usa en proyectos Vite, React y Astro cuando el objetivo es reducir peso sin romper pantallas.
flowchart LR
A["source files"] --> B["ESM import/export graph"]
B --> C["bundler tree shaking"]
C --> D["minified production bundle"]
B --> E["side effects kept"]
E --> D
D --> F["measure bytes and gzip"]
Usa documentación oficial como base
Cada bundler tiene matices. Antes de tocar producción, conviene apoyarse en documentación oficial.
| Tema | Enlace oficial | Qué revisar |
|---|---|---|
| webpack | Tree Shaking | sideEffects, ESM y build de producción |
| opción webpack | optimization.sideEffects | cómo lee webpack el campo sideEffects |
| Rollup/Vite | Rollup treeshake | evitar cambios globales agresivos |
| detalle Rollup | treeshake.moduleSideEffects | conservar módulos con inicialización |
| esbuild | Tree shaking | análisis ESM y medición con metafile |
La idea clave es que tree shaking no borra texto al azar. Sigue un grafo ESM estático y conserva código si eliminarlo podría cambiar el comportamiento en runtime. CommonJS, namespace imports, objetos default llenos de helpers y módulos que importan CSS o polyfills en el nivel superior suelen limitar el resultado.
Prompt recomendado para Claude Code
Empieza con investigación, no con cambios. Una configuración global mal aplicada puede ocultar regresiones visuales o de inicialización.
Investiga por qué el tree shaking es débil en el bundle de producción.
Primero entrega una tabla con tamaño actual, chunks principales, dependencias pesadas,
dependencias CommonJS y barrel exports.
Para cada cambio propuesto, incluye riesgo, impacto esperado y comandos de verificación.
CSS, polyfills, analytics y global setup no deben eliminarse.
Cuando toque modificar, acota el alcance.
En esta pasada trabaja solo en src/utils y src/components/index.ts.
Cambia default object exports a named exports y actualiza los imports.
Después ejecuta npm run build y la medición de tamaño de bundle.
Si se afecta una API pública, conserva un re-export compatible.
Así Claude Code optimiza alrededor de “qué comportamiento se mantiene” y no solo alrededor de “cuánto código se eliminó”.
Ejemplo mínimo ejecutable
Este proyecto pequeño compara un default object export con named exports usando esbuild.
mkdir tree-shaking-lab
cd tree-shaking-lab
npm init -y
npm install --save-dev esbuild
mkdir src scripts
Usa este package.json.
{
"name": "tree-shaking-lab",
"version": "1.0.0",
"type": "module",
"private": true,
"sideEffects": false,
"scripts": {
"measure": "node scripts/measure-tree-shaking.mjs"
},
"devDependencies": {
"esbuild": "^0.25.0"
}
}
La versión menos favorable junta helpers en un objeto.
// src/bad-utils.ts
const utils = {
formatEur(amount: number): string {
return new Intl.NumberFormat("es-ES", {
style: "currency",
currency: "EUR"
}).format(amount);
},
heavyReport(rows: number[]): string {
const body = rows.map((row) => `row:${row}`).join("\n");
return `report\n${body}\n${"=".repeat(4000)}`;
},
debugOnly(): string {
return "debug:" + "x".repeat(4000);
}
};
export default utils;
La versión más fácil de analizar exporta cada función.
// src/good-utils.ts
export function formatEur(amount: number): string {
return new Intl.NumberFormat("es-ES", {
style: "currency",
currency: "EUR"
}).format(amount);
}
export function heavyReport(rows: number[]): string {
const body = rows.map((row) => `row:${row}`).join("\n");
return `report\n${body}\n${"=".repeat(4000)}`;
}
export function debugOnly(): string {
return "debug:" + "x".repeat(4000);
}
Crea dos entradas.
// src/bad-entry.ts
import utils from "./bad-utils";
console.log(utils.formatEur(1200));
// src/good-entry.ts
import { formatEur } from "./good-utils";
console.log(formatEur(1200));
Añade el script de medición.
// scripts/measure-tree-shaking.mjs
import { gzipSync } from "node:zlib";
import { build } from "esbuild";
async function bundle(entryPoint) {
const result = await build({
entryPoints: [entryPoint],
bundle: true,
minify: true,
format: "esm",
treeShaking: true,
write: false,
metafile: true
});
const code = result.outputFiles[0].text;
return {
entryPoint,
bytes: Buffer.byteLength(code),
gzipBytes: gzipSync(code).byteLength,
inputs: Object.keys(result.metafile.inputs)
};
}
const rows = await Promise.all([
bundle("src/bad-entry.ts"),
bundle("src/good-entry.ts")
]);
console.table(rows);
Ejecuta:
npm run measure
En un producto real añade nombres de chunks, gzip, Brotli y Total Blocking Time de Lighthouse. Cuando necesites saber qué dependencia quedó en el grafo, combina este flujo con la guía de análisis de bundle.
Caso de uso 1: ordenar utilidades
El primer ahorro suele aparecer en utils/index.ts o helpers.ts.
Si fechas, moneda, CSV, Markdown y depuración viven juntos, usar un helper puede arrastrar más código del necesario.
Pídele a Claude Code una refactorización pequeña.
Divide src/utils por propósito.
Cambia los usos a named imports y reexporta desde index.ts solo los helpers públicos.
Si hay Date.now, console, localStorage o fetch en el nivel superior,
muévelos dentro de funciones.
Una forma limpia sería:
// src/utils/formatDate.ts
export function formatDate(date: Date, locale = "es-ES"): string {
return new Intl.DateTimeFormat(locale).format(date);
}
// src/utils/index.ts
export { formatDate } from "./formatDate";
export { formatEur } from "./formatEur";
// src/pages/invoice.ts
import { formatEur } from "../utils/formatEur";
export function invoiceLabel(total: number): string {
return `Total: ${formatEur(total)}`;
}
Los barrel files no son malos por definición.
Se vuelven problemáticos cuando ejecutan inicialización, encadenan muchos export * from o empujan módulos no relacionados al grafo.
En código de aplicación, importa directo; en librerías públicas, conserva un barrel delgado si necesitas compatibilidad.
Caso de uso 2: librerías internas de UI
En una librería interna, import { Button } from "@acme/ui" puede evaluar Modal, DatePicker, Chart, iconos, CSS y setup de tema.
Si todos los componentes comparten una entrada grande, los named exports no bastan.
Divide entradas con subpaths.
{
"name": "@acme/ui",
"type": "module",
"sideEffects": [
"**/*.css",
"./src/setup-theme.ts"
],
"exports": {
".": "./dist/index.js",
"./button": "./dist/button.js",
"./modal": "./dist/modal.js"
}
}
El consumidor importa solo lo necesario.
import { Button } from "@acme/ui/button";
No uses sideEffects: false sin revisar.
Ese campo indica que importar un módulo no ejecuta trabajo necesario en el exterior.
CSS, polyfills, registro de custom elements y setup de tema deben quedar listados si deben ejecutarse.
Caso de uso 3: cargar dependencias pesadas solo en admin
Procesadores Markdown, PDF, gráficos y editores ricos no suelen ser necesarios en la primera página pública. Aplica tree shaking para quitar exports no usados y code splitting para mover esas funciones a chunks tardíos.
// src/features/admin/loadMarkdownPreview.ts
export async function renderMarkdown(markdown: string): Promise<string> {
const [{ unified }, remarkParse, remarkHtml] = await Promise.all([
import("unified"),
import("remark-parse"),
import("remark-html")
]);
const file = await unified()
.use(remarkParse.default)
.use(remarkHtml.default)
.process(markdown);
return String(file);
}
Dynamic import no reemplaza al tree shaking. Mueve código a un chunk posterior; no garantiza que ese chunk sea pequeño. Mide por separado lo que salió del bundle inicial y lo que se eliminó dentro del chunk lazy.
Caso de uso 4: publicar un paquete npm
Si publicas una librería, expón entradas ESM que el bundler del consumidor pueda analizar.
Solo ofrecer un main CommonJS dificulta el tree shaking en aplicaciones frontend.
{
"name": "@masa/formatters",
"type": "module",
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./currency": {
"types": "./dist/currency.d.ts",
"import": "./dist/currency.js"
}
}
}
Usa sideEffects: false solo si el paquete no tiene efectos al importarse.
Si importa CSS, instala polyfills, registra globals o inicia analytics, lista esos archivos en el array sideEffects.
Fallos y trampas comunes
| Trampa | Síntoma | Solución |
|---|---|---|
| Babel o TypeScript emiten CommonJS pronto | exports no usados permanecen | conserva ESM hasta el bundler |
sideEffects: false demasiado amplio | desaparecen CSS o polyfills | lista archivos con efectos |
| default object export | helpers no usados quedan dentro | usa named exports |
| barrel con setup en nivel superior | importar un componente pesa mucho | barrel solo para re-export |
| medir en dev build | números engañosos | compara producción, minify y gzip |
moduleSideEffects: false global | desaparece inicialización | valida por paquete o archivo |
| namespace import | análisis más conservador | usa named imports concretos |
Los fallos visuales sutiles son los más peligrosos. Un test que solo verifica que existe el DOM puede pasar aunque falte CSS. Trata esta tarea como optimización de rendimiento: build, pantallas clave y comportamiento visible.
Presupuesto de bundle en CI
Si no se mide en CI, el tamaño vuelve a crecer. Este script falla cuando el gzip supera el presupuesto.
// scripts/check-bundle-budget.mjs
import { statSync } from "node:fs";
import { gzipSync } from "node:zlib";
import { readFileSync } from "node:fs";
const file = "dist/assets/index.js";
const maxGzipBytes = 160 * 1024;
const raw = readFileSync(file);
const gzipBytes = gzipSync(raw).byteLength;
if (gzipBytes > maxGzipBytes) {
console.error(`Bundle budget exceeded: ${gzipBytes} > ${maxGzipBytes}`);
process.exit(1);
}
console.log({
file,
bytes: statSync(file).size,
gzipBytes
});
Ejecútalo después del build.
npm run build
node scripts/check-bundle-budget.mjs
No pongas el primer presupuesto como una meta irreal. Empieza cerca del gzip actual con algo de margen y exige explicación cuando un PR lo aumente. Si la app sigue lenta, revisa imágenes, fuentes, latencia de API e hidratación con la guía de speed optimization.
Checklist de revisión para Claude Code
Revisa este PR de tree shaking.
1. ¿Los exports no usados desaparecieron del bundle de producción?
2. ¿Se conservaron CSS, polyfills y archivos de registro?
3. ¿ESM se mantuvo hasta la fase que analiza el bundler?
4. ¿Los imports directos rompieron alguna API pública?
5. ¿Cuáles son los resultados de build, tests, pantallas clave y bundle budget?
Incluye archivos y evidencia de comandos en cada punto.
Este checklist convierte una refactorización en una revisión publicable.
En trabajos de Masa, un cambio en sideEffects no se da por terminado hasta abrir login, facturación y administración para confirmar estilos e inicialización.
Impacto en monetización
Tree shaking no es solo limpieza técnica. Una carga inicial más ligera reduce fricción antes de leer artículos, abrir páginas de producto, registrarse o enviar un formulario de consultoría. En un sitio técnico como ClaudeCodeLab, una página de ejemplo o landing lenta debilita el camino hacia anuncios y consultas.
ClaudeCodeLab puede auditar bundles de Vite, Next.js, Astro y librerías internas de UI, y convertir hallazgos en tree shaking, code splitting y presupuestos de CI.
Para una consulta enfocada, trae package.json, configuración de build, rutas clave y un reporte reciente de bundle.
Resumen
Tree shaking funciona cuando ESM, sideEffects preciso, efectos controlados y medición continua están alineados.
Claude Code es útil si le das tareas pequeñas y verificables: investigar, dividir, actualizar imports, medir y revisar fallos.
Ejecuté el ejemplo mínimo de este artículo con npm run measure y confirmé que las entradas bad y good generan tamaños distintos.
En proyectos reales, los números dependen de dependencias y configuración; mide siempre tu production build y documenta qué efectos secundarios deben conservarse.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Permission receipt para Claude Code: alcance, prueba y rollback
Patrón de permission receipt para Claude Code: acciones permitidas, aprobación, pruebas, rollback y CTA de ingresos.
Agent Harness seguro para Claude Code y Codex: permisos, verificacion y rollback
Diseña un Agent Harness seguro para Claude Code y Codex con permisos, plan, verificaciones y rollback.
Subagentes de Claude Code: guía práctica para delegar trabajo de forma segura
Guía práctica de subagentes en Claude Code para dividir artículos y código: reglas, prompts, riesgos y checklist.