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 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.
| Thema | Offizieller Link | Worauf es ankommt |
|---|---|---|
| webpack | Tree Shaking | sideEffects, ESM, Production Build |
| webpack-Option | optimization.sideEffects | wie webpack package-Flags liest |
| Rollup/Vite | Rollup treeshake | keine groben globalen Abschaltungen |
| Rollup-Detail | treeshake.moduleSideEffects | Setup-Module erhalten |
| esbuild | Tree shaking | ESM-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
| Falle | Symptom | Lösung |
|---|---|---|
| Babel oder TypeScript erzeugt zu früh CommonJS | ungenutzte Exports bleiben | ESM bis zum Bundler erhalten |
sideEffects: false zu breit | CSS oder Polyfills fehlen | Side-Effect-Dateien auflisten |
| Default-Object-Export | ungenutzte Helper bleiben | Named Exports verwenden |
| Barrel mit Top-Level-Setup | ein Component-Import wird teuer | Barrel nur als Re-Export |
| Messung im Dev Build | Zahlen sind irreführend | Production, minify, gzip vergleichen |
globales moduleSideEffects: false | Setup-Code verschwindet | pro Paket oder Datei prüfen |
| Namespace-Import | Analyse wird konservativer | gezielte 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.
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.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude-Code-Permission-Receipt: Scope, Beweis und Rollback festhalten
Permission-Receipt für Claude Code: erlaubte Aktionen, Freigabegrenzen, Prüfbefehle, Rollback und Umsatz-CTA-Prüfung.
Sicheres Agent Harness fur Claude Code und Codex: Rechte, Prufung und Rollback
Ein praktisches Agent Harness fur Claude Code und Codex mit Policy, Plan, Verifikation und Recovery.
Claude Code Subagents: Praxisleitfaden für sichere Agent-Delegation
Claude Code Subagents praktisch nutzen: Artikel- und Codearbeit sicher aufteilen, Prompts einsetzen, Fehler vermeiden.