Advanced (Aktualisiert: 2.6.2026)

Tree Shaking mit Claude Code: Bundles messbar verkleinern

Tree Shaking mit Claude Code verbessern: ESM, sideEffects, Messung, typische Fehler und ausführbare Beispiele.

Tree Shaking mit Claude Code: Bundles messbar verkleinern

Tree Shaking einfach erklärt

Tree Shaking entfernt beim Production Build ungenutzte JavaScript- oder TypeScript-Exports aus dem finalen Bundle. Praktisch heißt das: Der Browser soll keinen Code laden, parsen und ausführen, den die aktuelle Seite nicht braucht. Gerade bei Content-Seiten, SaaS-Dashboards und Admin-Oberflächen kann das den ersten Seitenaufruf spürbar entlasten.

Ein Bundler kennt aber nicht die Absicht des Teams. Er bewertet import, export, das sideEffects-Feld in package.json, CommonJS-Umwandlungen und Code, der schon beim Import eines Moduls auf oberster Ebene läuft. Deshalb entstehen typische Überraschungen: ungenutzte Helper bleiben im Bundle, oder sideEffects: false entfernt versehentlich CSS oder Polyfills.

Claude Code ist dann hilfreich, wenn die Aufgabe messbar ist. Statt “mach die App kleiner” sollte es zuerst aktuelle Größen erfassen, CommonJS-Abhängigkeiten finden, Barrel Files prüfen, Seiteneffekte einordnen, kleine Änderungen machen und danach den Production Build verifizieren. So arbeitet Masa bei Vite-, React- und Astro-Projekten, wenn die Bundle-Größe reduziert werden soll, ohne Verhalten zu beschädigen.

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

Offizielle Dokumentation als Grundlage

Tree Shaking verhält sich je nach Bundler unterschiedlich. Vor Änderungen an Produktivcode sollten diese offiziellen Quellen die Basis sein.

ThemaOffizieller LinkWorauf es ankommt
webpackTree ShakingsideEffects, ESM, Production Build
webpack-Optionoptimization.sideEffectswie webpack package-Flags liest
Rollup/ViteRollup treeshakekeine groben globalen Abschaltungen
Rollup-Detailtreeshake.moduleSideEffectsSetup-Module erhalten
esbuildTree shakingESM-Analyse und Messung per metafile

Wichtig ist: Tree Shaking löscht nicht beliebig Text. Es folgt einem statischen ESM-Abhängigkeitsgraphen und behält Code, wenn dessen Entfernung Laufzeitverhalten ändern könnte. CommonJS, Namespace-Imports, große Default-Objekte und Top-Level-Imports von CSS oder Polyfills machen die Analyse konservativer.

Ein nützlicher Prompt für Claude Code

Beginne mit Analyse, nicht mit Änderungen. Ein global gesetztes sideEffects: false kann visuelle Fehler oder fehlende Initialisierung erst spät sichtbar machen.

Untersuche, warum Tree Shaking im Production Bundle dieses Repos schwach ist.
Erstelle zuerst eine Tabelle mit aktueller Build-Größe, wichtigsten Chunks,
schweren Dependencies, CommonJS-Dependencies und Barrel Exports.
Für jede vorgeschlagene Änderung nenne Risiko, erwartete Größenwirkung und Prüfkommandos.
CSS, Polyfills, Analytics und Global Setup dürfen nicht entfernt werden.

Für die Umsetzung wird der Bereich begrenzt.

Bearbeite in dieser Runde nur src/utils und src/components/index.ts.
Wandle Default-Object-Exports in Named Exports um und aktualisiere die Imports.
Führe danach npm run build und die Bundle-Messung aus.
Wenn eine öffentliche API betroffen ist, behalte einen kompatiblen Re-Export.

Damit optimiert Claude Code nicht blind auf Entfernung, sondern auf kleinere Bundles bei stabilem Verhalten.

Kopierbares Minimalbeispiel

Dieses kleine esbuild-Projekt vergleicht einen Default-Object-Export mit Named Exports.

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

Nutze diese 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"
  }
}

Die schwächere Variante packt Helper in ein Objekt.

// src/bad-utils.ts
const utils = {
  formatEur(amount: number): string {
    return new Intl.NumberFormat("de-DE", {
      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;

Die bessere Variante exportiert Funktionen einzeln.

// src/good-utils.ts
export function formatEur(amount: number): string {
  return new Intl.NumberFormat("de-DE", {
    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);
}

Erstelle zwei Entry-Dateien.

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

Messskript:

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

Ausführen:

npm run measure

In echten Projekten solltest du zusätzlich Chunk-Namen, gzip, Brotli und Lighthouse Total Blocking Time erfassen. Wenn du wissen musst, welche Dependency im Graphen bleibt, kombiniere diese Messung mit dem Artikel zur Bundle-Analyse.

Use Case 1: Utility-Module aufräumen

Der schnellste Gewinn liegt oft in utils/index.ts oder helpers.ts. Wenn Datum, Währung, CSV, Markdown und Debugging in einer Datei gemischt sind, kann eine kleine Funktion unnötig viel Analysefläche erzeugen.

Claude Code kann so beauftragt werden:

Teile src/utils nach Zweck auf.
Stelle Verbraucher auf Named Imports um und re-exportiere aus index.ts nur öffentliche Helper.
Falls Top-Level-Aufrufe von Date.now, console, localStorage oder fetch existieren,
verschiebe sie in Funktionen.

Eine saubere Form:

// src/utils/formatDate.ts
export function formatDate(date: Date, locale = "de-DE"): 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 `Summe: ${formatEur(total)}`;
}

Barrel Files sind nicht automatisch schlecht. Problematisch werden sie, wenn sie Setup ausführen, breite export * from-Ketten bilden oder unzusammenhängende Module in den Graphen ziehen. Im App-Code sind direkte Imports meist besser; für öffentliche Libraries kann ein dünner Barrel aus Kompatibilitätsgründen sinnvoll bleiben.

Use Case 2: Interne UI-Libraries

In UI-Paketen kann import { Button } from "@acme/ui" im Hintergrund Modal, DatePicker, Chart, Icon-Sets, CSS und Theme-Setup auswerten. Wenn alle Komponenten eine große Entry-Datei teilen, reichen Named Exports nicht aus.

Teile das Paket in 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"
  }
}

Der Consumer importiert nur den benötigten Einstieg.

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

sideEffects: false darf hier nicht blind gesetzt werden. Das Feld beschreibt, ob ein Modul beim Import notwendige Seiteneffekte ausführt. CSS, Polyfills, Custom-Element-Registrierung und Theme-Setup müssen im sideEffects-Array bleiben, wenn sie gebraucht werden.

Use Case 3: Schwere Admin-Abhängigkeiten verzögert laden

Markdown-Verarbeitung, PDF-Generatoren, Charts und Rich-Text-Editoren werden oft nicht beim ersten öffentlichen Seitenaufruf gebraucht. Nutze Tree Shaking für ungenutzte Exports und Code Splitting, um Admin-Funktionen aus dem initialen Chunk zu entfernen.

// 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 ersetzt Tree Shaking nicht. Er verschiebt Code nur in einen späteren Chunk; wenn dieser Chunk CommonJS-lastig bleibt, kann er trotzdem groß sein.

Use Case 4: npm-Paket veröffentlichen

Wenn du eine Library veröffentlichst, gib dem Bundler der Nutzer eine analysierbare ESM-Struktur. Ein reiner CommonJS-main-Eintrag ist für Frontend-Bundles meist ungünstig.

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

Nutze sideEffects: false nur, wenn das Paket wirklich keine Import-Seiteneffekte hat. CSS, Polyfills, globale Registrierung oder Analytics-Start müssen explizit im Array stehen.

Häufige Fehler und Fallen

FalleSymptomLösung
Babel oder TypeScript erzeugt zu früh CommonJSungenutzte Exports bleibenESM bis zum Bundler erhalten
sideEffects: false zu breitCSS oder Polyfills fehlenSide-Effect-Dateien auflisten
Default-Object-Exportungenutzte Helper bleibenNamed Exports verwenden
Barrel mit Top-Level-Setupein Component-Import wird teuerBarrel nur als Re-Export
Messung im Dev BuildZahlen sind irreführendProduction, minify, gzip vergleichen
globales moduleSideEffects: falseSetup-Code verschwindetpro Paket oder Datei prüfen
Namespace-ImportAnalyse wird konservativergezielte Named Imports nutzen

Besonders gefährlich sind leise visuelle Regressionen. Ein Test kann bestehen, obwohl CSS fehlt, wenn er nur DOM-Knoten prüft. Behandle die Änderung wie Performance-Optimierung: Build, wichtige Screens und sichtbares Verhalten prüfen.

Bundle-Budget in CI

Ohne Budget wächst das Bundle beim nächsten Dependency-Update wieder. Ein einfacher gzip-Check reicht als Start.

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

Nach dem Production Build ausführen:

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

Setze das erste Budget nicht auf einen Wunschwert. Starte nahe der heutigen gzip-Größe mit etwas Luft und verlange bei PR-Zuwachs eine Begründung. Wenn die App weiterhin langsam wirkt, prüfe zusätzlich Bilder, Fonts, API-Latenz und Hydration mit dem Speed-Optimization-Guide.

Review-Checkliste für Claude Code

Reviewe diesen Tree-Shaking-PR.
1. Sind ungenutzte Exports wirklich aus dem Production Bundle verschwunden?
2. Wurden CSS, Polyfills und Registrierungsdateien erhalten?
3. Blieb ESM bis zur Bundler-Analyse erhalten?
4. Brechen direkte Imports öffentliche API-Kompatibilität?
5. Welche Ergebnisse haben Build, Tests, wichtige Screens und Bundle Budget?
Bitte jede Antwort mit Datei- und Kommando-Evidenz belegen.

Damit wird ein Refactoring zu einer veröffentlichungsreifen Qualitätsprüfung. In Masas Projekten ist eine sideEffects-Änderung erst fertig, wenn Login-, Billing- und Admin-Screens auf fehlendes Styling oder fehlende Initialisierung geprüft wurden.

Monetarisierungsblick

Tree Shaking ist nicht nur technische Sauberkeit. Ein leichter initialer Load reduziert Reibung vor Artikelansichten, Produktseiten, Registrierungen und Kontaktformularen. Bei einer technischen Website wie ClaudeCodeLab schwächt ein schwerer Codebeispiel- oder Beratungs-Landing-Page direkt den Weg zu Anzeigen- und Beratungsumsätzen.

ClaudeCodeLab kann Vite-, Next.js-, Astro- und interne UI-Library-Bundles prüfen und daraus Tree-Shaking-Fixes, Code Splitting und CI-Budgets ableiten. Für eine gezielte Beratung helfen package.json, Build-Konfiguration, wichtige Routes und ein aktueller Bundle Report.

Fazit

Tree Shaking funktioniert, wenn ESM, präzise sideEffects, kontrollierte Seiteneffekte und kontinuierliche Messung zusammenpassen. Claude Code sollte dafür kleine, überprüfbare Aufgaben bekommen: analysieren, aufteilen, Imports korrigieren, messen und Fehlerfälle prüfen.

Ich habe das Minimalbeispiel dieses Artikels lokal mit npm run measure ausgeführt und bestätigt, dass bad entry und good entry unterschiedliche Ausgaben erzeugen. In echten Projekten hängen die Zahlen von Dependencies und Build-Konfiguration ab; miss immer deinen eigenen Production Build und dokumentiere zuerst, welche Seiteneffekte erhalten bleiben müssen.

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

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.