Advanced (Updated: 6/2/2026)

Tree Shaking with Claude Code: Practical Bundle Cleanup

Improve tree shaking with Claude Code using ESM, sideEffects, bundle measurement, pitfalls, and runnable examples.

Tree Shaking with Claude Code: Practical Bundle Cleanup

What tree shaking really means

Tree shaking is the production-build step that removes unused JavaScript or TypeScript exports from the final bundle. The name sounds abstract, but in day-to-day work it means one simple thing: do not ship code the current page does not need.

Bundlers cannot read your intent. They inspect import and export syntax, the sideEffects field in package.json, CommonJS transforms, and top-level code that runs as soon as a module is imported. That is why teams see confusing failures such as “the unused helper stayed in the bundle” or “adding sideEffects: false removed my CSS.”

Claude Code helps most when you give it a measurable workflow, not a vague request to “make the app lighter.” Ask it to capture the current bundle size, identify ESM and CommonJS boundaries, inventory side-effectful files, change one area at a time, and verify the build result. This article uses a small runnable sample and the same review checklist Masa uses when cleaning up Vite, React, and Astro projects.

The working model looks like this:

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"]

Start from official behavior, not guesses

Tree shaking behavior differs by bundler. Use official docs as your baseline before changing production code.

TopicOfficial linkWhat to check in practice
webpackTree ShakingsideEffects, ESM syntax, production builds
webpack optionoptimization.sideEffectshow webpack reads package sideEffects flags
Rollup/ViteRollup treeshakeavoid broad moduleSideEffects changes
Rollup detailstreeshake.moduleSideEffectskeep modules that run setup code
esbuildTree shakingESM analysis and metafile-based measurement

The important point is that tree shaking is not magic string deletion. It follows a static ESM dependency graph and keeps code when removing it could change runtime behavior. CommonJS-heavy modules, default objects full of utilities, namespace imports, and files that import CSS or polyfills at the top level often survive longer than expected.

A better Claude Code prompt

Start with investigation and measurement, not edits. Dropping a global sideEffects: false into a large app can hide visual and runtime regressions.

Investigate why tree shaking is weak in this repository's production bundle.
First report the current build size, major chunks, heavy dependencies,
CommonJS dependencies, and barrel exports in a table.
For every proposed change, include risk, expected size impact, and verification commands.
Assume CSS, polyfills, analytics, and global setup files must not be removed.

When you are ready for a change, narrow the scope.

Only work on src/utils and src/components/index.ts for this pass.
Convert default object exports to named exports and update importing files.
After the change, run npm run build and the bundle-size measurement.
If this changes a public API, keep a compatibility re-export.

That framing makes Claude Code optimize around “what stayed correct” rather than only “what was removed.”

Copy-paste runnable measurement sample

Tree shaking should be measured, not guessed. This small esbuild project compares a default object export with named exports.

mkdir tree-shaking-lab
cd tree-shaking-lab
npm init -y
npm install --save-dev esbuild
mkdir src scripts

Use this 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"
  }
}

The weak version hides many helpers inside one object.

// src/bad-utils.ts
const utils = {
  formatUsd(amount: number): string {
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD"
    }).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;

The stronger version exports each helper separately.

// src/good-utils.ts
export function formatUsd(amount: number): string {
  return new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: "USD"
  }).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);
}

Create two entry files.

// src/bad-entry.ts
import utils from "./bad-utils";

console.log(utils.formatUsd(1200));
// src/good-entry.ts
import { formatUsd } from "./good-utils";

console.log(formatUsd(1200));

Add the measurement script.

// 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);

Run it.

npm run measure

The exact bytes are less important than the lesson: the same visible feature can produce different bundle output. In a real app, add chunk names, gzip size, Brotli size, and Lighthouse Total Blocking Time. Pair this workflow with the bundle analysis guide when you need to see which dependency stayed in the graph.

Use case 1: split utility modules

The fastest win is often a crowded utils/index.ts or helpers.ts. Date formatting, currency formatting, CSV export, Markdown rendering, and debug helpers should not all live in one object.

Ask Claude Code to make the change in small steps.

Split src/utils by purpose.
Replace imports with named imports and only re-export the public helpers from index.ts.
If any top-level Date.now, console, localStorage, or fetch calls exist,
move them inside functions.

A cleaner shape looks like this.

// src/utils/formatDate.ts
export function formatDate(date: Date, locale = "en-US"): string {
  return new Intl.DateTimeFormat(locale).format(date);
}
// src/utils/index.ts
export { formatDate } from "./formatDate";
export { formatUsd } from "./formatUsd";
// src/pages/invoice.ts
import { formatUsd } from "../utils/formatUsd";

export function invoiceLabel(total: number): string {
  return `Total: ${formatUsd(total)}`;
}

Barrel files are not automatically bad. They become risky when they run setup code, chain broad export * from statements, or force unrelated modules into the graph. For application code, prefer direct imports; for library consumers, keep a thin public barrel if compatibility requires it.

Use case 2: internal UI libraries

Internal UI packages often expose import { Button } from "@acme/ui" while evaluating Modal, DatePicker, Chart, icon sets, CSS, and theme setup behind the scenes. Named exports alone may not be enough if every component shares one heavy entry file.

Split the package into subpath entries.

{
  "name": "@acme/ui",
  "type": "module",
  "sideEffects": [
    "**/*.css",
    "./src/setup-theme.ts"
  ],
  "exports": {
    ".": "./dist/index.js",
    "./button": "./dist/button.js",
    "./modal": "./dist/modal.js"
  }
}

Then import only the needed entry.

import { Button } from "@acme/ui/button";

Be careful with sideEffects: false. It tells the bundler that importing files from the package does not run necessary setup. That is a module-level concept, not the same thing as React’s useEffect. CSS files, custom element registration, polyfills, and theme setup should be listed as side-effectful when they must run.

Use case 3: lazy-load admin-only dependencies

Markdown processors, PDF generators, charts, and rich text editors are often unnecessary on the first public page load. Use tree shaking to remove unused exports, then use code splitting to keep heavy features out of the initial chunk.

// 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 is not a replacement for tree shaking. It moves code into a later chunk; it does not guarantee that the later chunk is small. Measure “removed from initial bundle” and “unused code removed inside the lazy chunk” as separate outcomes.

Use case 4: publish a tree-shakable npm package

If you publish a package, give consumers an ESM shape their bundlers can analyze. Avoid exposing only a CommonJS main entry when your users build frontend code.

{
  "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"
    }
  }
}

Only use sideEffects: false when the package is truly side-effect free. If it imports CSS, registers globals, installs polyfills, or starts analytics, list those files in the sideEffects array instead.

Pitfalls and failure cases

Most failures are predictable. Have Claude Code review them in a table before merging.

PitfallSymptomFix
Babel or TypeScript emits CommonJS too earlyunused exports remainkeep ESM until the bundler step
sideEffects: false is too broadCSS or polyfills disappearlist side-effectful files explicitly
default object exportunused helpers stay with object creationsplit into named exports
barrel file runs top-level setupimporting one component is expensivekeep barrels as re-export-only files
measuring dev buildssize changes look wrongcompare production, minified, gzip output
global moduleSideEffects: falsesetup code is droppedvalidate per package or per file
namespace importsanalysis becomes conservativeimport named bindings directly

The dangerous failures are subtle visual ones. A test that only checks DOM existence may pass while a CSS import has disappeared. Treat bundle cleanup like performance optimization: verify build output, key screens, and user-visible behavior.

Put bundle budgets in CI

A one-time cleanup will regress on the next dependency update. Add a small budget check so every PR sees the cost.

// 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
});

Run it after a production build.

npm run build
node scripts/check-bundle-budget.mjs

Do not set the first budget to a fantasy target. Start near today’s gzip size with a little room, then require a short explanation when a PR increases it. If the app still feels slow after bundle cleanup, also inspect images, fonts, API latency, and hydration with the speed optimization guide.

Claude Code review checklist

After implementation, use this prompt as a review gate.

Review this tree-shaking PR.
1. Did unused exports disappear from the production bundle?
2. Were CSS, polyfills, and registration files preserved?
3. Was ESM kept until the bundler could analyze it?
4. Did direct imports break any public API compatibility?
5. What are the build, test, key-screen, and bundle-budget results?
Report file names and command evidence for each answer.

This turns the task from a mechanical refactor into a publishable quality check. In Masa’s client work, the sideEffects change is never considered done until login, billing, and admin screens are opened and checked for missing styling or setup behavior.

Monetization angle

Tree shaking is not only a technical cleanup. Faster first loads reduce friction before article views, product pages, signup forms, and consulting contact forms. For a technical media site like ClaudeCodeLab, a heavy code-example page or slow landing page weakens the revenue path.

ClaudeCodeLab can audit Vite, Next.js, Astro, and internal UI-library bundles, then turn findings into tree-shaking fixes, code splitting, and CI bundle budgets. For a focused consultation, bring package.json, build config, key routes, and a recent bundle report.

Summary

Tree shaking works when ESM, accurate sideEffects, contained setup code, and continuous measurement line up. Claude Code is useful when you give it a narrow measured task: inspect, split, update imports, verify, and review the failure cases.

I ran the minimal sample from this article through npm run measure locally and confirmed that the bad and good entries produce different output sizes. In real projects, the numbers depend on your dependencies, so measure your own production build and document which side effects must remain before publishing the change.

#Claude Code #tree shaking #bundle size #ES Modules #frontend optimization
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.