Bundle-Größe durch Dead Code Elimination halbieren
Tree Shaking ist der Prozess, mit dem moderne Bundler ungenutzten JavaScript-Code aus dem finalen Bundle entfernen. Was simpel klingt, hat komplexe Voraussetzungen: ES-Module sind Pflicht, bestimmte Code-Muster blockieren Tree Shaking, und falsch konfigurierte sideEffects-Flags können gesamte Module eliminieren oder bewahren — mit messbaren Auswirkungen auf Ladezeiten.
Inhaltsverzeichnis
- 1. Was Tree Shaking ist und warum es wichtig ist
- 2. Warum ES-Module Pflicht sind: statische Import-Analyse
- 3. Das sideEffects-Flag in package.json
- 4. Code-Muster, die Tree Shaking verhindern
- 5. Tree-Shaking-freundliche Bibliotheken schreiben
- 6. Tree Shaking mit Webpack und Rollup konfigurieren
- 7. Tree Shaking: ESM vs. CJS vs. IIFE im Vergleich
- 8. Bundle-Analyse: toten Code mit Tools sichtbar machen
- 9. Scope Hoisting: der Partner von Tree Shaking
- 10. Zusammenfassung
- 11. FAQ
1. Was Tree Shaking ist und warum es wichtig ist
Tree Shaking ist die Technik, mit der JavaScript-Bundler ungenutzte Exporte und deren Abhängigkeiten aus dem finalen Bundle entfernen. Der Name kommt vom Bild, einen Baum zu schütteln — was nicht befestigt ist, fällt herunter. In der Praxis bedeutet das: Wenn eine Anwendung nur format aus einer Utility-Bibliothek mit hundert Funktionen importiert, enthält das finale Bundle nur format und dessen Abhängigkeiten — nicht die anderen 99 Funktionen. Das reduziert die Bundle-Größe direkt proportional zum Anteil ungenutzten Codes.
Die Auswirkungen auf Ladezeiten sind erheblich. Jedes Kilobyte JavaScript bedeutet Parse-Zeit, Kompilierungszeit und Übertragungszeit — besonders auf mobilen Geräten und schwachen Netzwerken. Bibliotheken wie lodash haben in ihrer CJS-Version mehrere hundert Kilobyte; mit korrektem Tree Shaking landet nur der tatsächlich genutzte Code im Bundle. Das ist kein akademisches Detail: Studien zeigen, dass jede Sekunde Ladezeit die Conversion-Rate um mehrere Prozent senkt. Tree Shaking ist damit nicht nur eine technische Optimierung, sondern direkt geschäftsrelevant.
2. Warum ES-Module Pflicht sind: statische Import-Analyse
Der Kern von Tree Shaking ist die statische Analyse von Import- und Export-Beziehungen. Nur wenn ein Bundler zur Build-Zeit — ohne Code auszuführen — bestimmen kann, welche Exporte wirklich verwendet werden, kann er sicher entscheiden, welche Teile er entfernen darf. ES-Module ermöglichen das: import und export sind statische Konstrukte — sie stehen immer auf der obersten Ebene, können nicht bedingt sein und müssen statische String-Literale als Pfade haben. Das erlaubt dem Bundler, den vollständigen Import-Export-Graphen zu konstruieren, bevor eine einzige Zeile ausgeführt wird.
CommonJS-Module (require() und module.exports) sind dagegen dynamisch: require() kann zur Laufzeit mit einem berechneten Pfad aufgerufen werden, Exporte können zur Laufzeit als Properties von module.exports hinzugefügt werden. Das macht statische Analyse unmöglich. Ein Bundler, der auf ein CJS-Modul trifft, muss das gesamte Modul in das Bundle aufnehmen — er kann nicht wissen, welche Exporte in einem fremden Runtime-Kontext genutzt werden. Tree Shaking funktioniert deshalb ausschließlich mit ESM — das ist keine Designentscheidung der Bundler, sondern eine mathematische Notwendigkeit aus der Dynamik von CJS.
// ESM: static — bundler can analyze at build time WITHOUT executing code
import { formatDate, parseDate } from "./date-utils.js";
// Bundler knows: only formatDate and parseDate are used
// → everything else in date-utils.js is candidate for tree-shaking
// CJS: dynamic — bundler cannot know what will be used at runtime
const utils = require("./date-utils"); // entire module included
utils.formatDate(new Date());
// OR: computed require — impossible to analyze statically
const fnName = "format" + "Date";
const fn = require("./date-utils")[fnName]; // bundler gives up
// ESM: conditional imports are forbidden — enables static graph
// This is INVALID in ESM (syntax error):
// if (condition) { import { x } from "./mod.js"; }
// Valid ESM alternative: dynamic import() — but affects tree-shaking
const { x } = await import(condition ? "./a.js" : "./b.js");
// Bundler includes both ./a.js and ./b.js when condition is dynamic
3. Das sideEffects-Flag in package.json
Das sideEffects-Feld in der package.json einer Bibliothek ist der wichtigste Hebel für Tree Shaking bei externen Abhängigkeiten. Ohne dieses Feld muss der Bundler konservativ sein: Ein importiertes Modul könnte Seiteneffekte beim Import haben (CSS-Injection, globale Registrierungen, Prototyp-Erweiterungen) und darf deshalb nicht entfernt werden, selbst wenn kein Export des Moduls verwendet wird. Mit "sideEffects": false signalisiert die Bibliothek: Kein Modul hat Seiteneffekte beim Import — der Bundler darf sicher alle ungenutzten Module entfernen.
"sideEffects": ["*.css", "polyfills.js"] ist das differenzierte Muster: Die meisten Dateien haben keine Seiteneffekte, aber CSS-Importe und die Polyfill-Datei müssen bewahrt werden. Das ist für Komponenten-Bibliotheken wie Material UI oder Ant Design kritisch — sie importieren CSS als Seiteneffekt, und ohne explizite Ausnahme im sideEffects-Array würde Tree Shaking diese CSS-Importe entfernen und das Styling kaputt machen. Als Bibliotheks-Autor ist das sideEffects-Feld Pflicht — ohne es liefert man seinen Nutzern schlechteres Tree Shaking als nötig.
4. Code-Muster, die Tree Shaking verhindern
Verschiedene JavaScript-Muster sind für Bundler bei der statischen Analyse undurchsichtig und verhindern effektives Tree Shaking. Das häufigste: Objekt-Exporte, bei denen alle Funktionen als Properties eines einzigen Exports gebündelt werden. export default { formatDate, parseDate, ... } exportiert ein Objekt — der Bundler kann nicht wissen, welche Properties des Objekts genutzt werden, und muss das gesamte Objekt beibehalten. Named Exports (export function formatDate()) ermöglichen dagegen präzises Tree Shaking auf Funktionsebene.
Ein weiteres kritisches Muster: IIFE-wrapped Module und Klassen mit statischen Methoden. Da Klassen in JavaScript mutierbar sind und statische Methoden zur Laufzeit hinzugefügt werden können, behandeln Bundler Klassen konservativ — sie behalten die gesamte Klasse, auch wenn nur eine statische Methode verwendet wird. Das Gegenpattern: Standalone-Funktionen als named Exports statt als statische Methoden. Wer eine Utility-Bibliothek schreibt, sollte bewusst auf Klassen-basierte APIs verzichten, wenn Tree Shaking wichtig ist.
// date-utils.js — tree-shaking friendly library design
// BAD: default export object — bundler cannot tree-shake individual functions
export default {
formatDate: (d) => d.toISOString(),
parseDate: (s) => new Date(s),
diffDays: (a, b) => Math.abs(a - b) / 86400000,
// 97 more functions — ALL included even if you only use formatDate
};
// GOOD: named exports — each function is individually tree-shakeable
export function formatDate(date) {
return date.toISOString();
}
export function parseDate(str) {
return new Date(str);
}
export function diffDays(dateA, dateB) {
return Math.abs(dateA - dateB) / 86400000;
}
// Consumer only imports what they need
import { formatDate } from "./date-utils.js";
// parseDate and diffDays are excluded from the bundle
// BAD: class with static methods — entire class is retained
export class DateUtils {
static formatDate(d) { return d.toISOString(); }
static parseDate(s) { return new Date(s); }
}
// import { DateUtils } from "./date-utils" then DateUtils.formatDate()
// → entire class bundle, even if only formatDate is used
// Pure annotation — hint to bundler that function has no side effects
export const expensiveCalc = /*#__PURE__*/ computeConstants();
// Without /*#__PURE__*/, bundler assumes function call has side effects
5. Tree-Shaking-freundliche Bibliotheken schreiben
Eine Tree Shaking-freundliche Bibliothek zu schreiben erfordert bewusste Entscheidungen auf mehreren Ebenen. Erstens: Nur Named Exports verwenden, niemals Default-Export-Objekte. Zweitens: "sideEffects": false in der package.json setzen, es sei denn, bestimmte Dateien haben tatsächlich Seiteneffekte. Drittens: Das ESM-Build als module-Einstiegspunkt in der package.json angeben, zusätzlich zum CJS-Build unter main. Bundler bevorzugen den module-Einstiegspunkt für Tree Shaking.
Der vierte wichtige Punkt: Keine Barrel-Dateien (index.js) mit Re-Exports aus vielen Untermodulen, wenn jedes Untermodul seiteneffektfrei ist. Barrel-Dateien, die hunderte von Exporten re-exportieren, machen dem Bundler die Analyse schwer und führen oft dazu, dass mehr Code als nötig eingeschlossen wird. Stattdessen: Deep Imports ermöglichen (import { formatDate } from "date-utils/date"`), oder die Barrel-Datei explizit als "sideEffects": false markieren und auf korrekte ESM-Struktur setzen, damit der Bundler die Re-Export-Kette auflösen kann.
6. Tree Shaking mit Webpack und Rollup konfigurieren
Rollup war der erste Bundler mit nativem Tree Shaking-Support und ist bis heute der Standard für Library-Builds. Rollup's Tree Shaking ist besonders aggressiv: Es analysiert den vollständigen Abhängigkeitsgraphen und entfernt alles, was nicht zum Output beiträgt. Für Anwendungsbundles mit Webpack: Tree Shaking ist in Production-Mode automatisch aktiv durch die Kombination aus mode: "production" (aktiviert TerserPlugin für Dead Code Elimination), optimization.usedExports: true (markiert nicht verwendete Exporte) und optimization.sideEffects: true (liest das sideEffects-Flag aus package.json).
Wichtig: Webpack's Tree Shaking funktioniert zweistufig. Zunächst markiert es ungenutzte Exporte mit einem Kommentar (/* unused harmony export */). In einem zweiten Schritt entfernt TerserPlugin diese markierten Exports tatsächlich aus dem minimierten Output. Ohne TerserPlugin (also im Development-Mode) sind ungenutzte Exporte im Bundle vorhanden, aber markiert — sichtbar im unminimiertem Build. Das ist wichtig für das Debugging: Wer Tree Shaking debuggen will, muss im Production-Mode bauen oder Terser explizit aktivieren.
7. Tree Shaking: ESM vs. CJS vs. IIFE im Vergleich
Die Wahl des Modul-Formats hat direkte Auswirkungen auf die Effektivität von Tree Shaking. ESM ermöglicht vollständiges Tree Shaking — der Bundler kann den gesamten Import-Export-Graphen statisch analysieren. CJS ist dynamisch und ermöglicht kein Tree Shaking auf Modul-Ebene — der Bundler muss das gesamte Modul einschließen. IIFE-Bundles sind unveränderbar — kein Bundler kann aus einem fertig gebündelten IIFE-Output weiteres Tree Shaking betreiben.
| Format | Tree Shaking möglich? | Typischer Einsatz | Bundler-Support |
|---|---|---|---|
| ESM (.mjs, type:module) | Vollständig | Alle modernen Apps und Libraries | Rollup, Webpack 5, Vite, esbuild |
| CJS (.cjs, require) | Nicht möglich | Legacy Node.js, alte Bibliotheken | Kein Bundler kann CJS shaken |
| UMD | Nicht möglich | Browser + Node kompatibel, veraltet | CJS-Pfad dominiert, kein Shaking |
| IIFE | Nicht möglich | Script-Tag Einbindung, CDN | Fertig gebündelt, nicht analysierbar |
| ESM + CJS dual | Über module-Feld | Moderne Libraries mit Rückwärtskompatibilität | Bundler nutzen module-Feld für ESM |
Für Library-Autoren ist das duale Publish-Pattern die Empfehlung: ESM-Build unter exports["."].import und module, CJS-Build unter exports["."].require und main. Bundler bevorzugen automatisch das ESM-Build und ermöglichen damit Tree Shaking. Node.js ohne Bundler nutzt das CJS-Build für Rückwärtskompatibilität. Mit dem neuen exports-Feld in package.json (Conditional Exports) ist das sauber konfigurierbar.
8. Bundle-Analyse: toten Code mit Tools sichtbar machen
Ohne Analyse-Tools ist es unmöglich zu wissen, ob Tree Shaking tatsächlich greift und was im Bundle steckt. Das wichtigste Tool für Webpack-Projekte ist webpack-bundle-analyzer — es erstellt eine interaktive Treemap, die zeigt, welche Module wie viel Platz im Bundle einnehmen. Große, unerwartete Blöcke zeigen sofort, wo Tree Shaking nicht funktioniert oder wo Bibliotheken ohne Tree-Shaking-Support eingebunden sind. Für Vite-Projekte bietet rollup-plugin-visualizer dieselbe Funktionalität.
Ein zweites wichtiges Tool ist bundle-buddy oder die Webpack-integrierte stats.json-Ausgabe, die zeigt, welche Module aus welchen Dateien kommen und wie viel Code aus jeder Abhängigkeit tatsächlich im Bundle landet. Für schnelle Checks ohne Build-Integration: bundlephobia.com zeigt für jede npm-Bibliothek, ob sie Tree-Shaking unterstützt und wie groß der minimale Import einer einzelnen Funktion ist. Das ist der erste Check, bevor eine neue Abhängigkeit ins Projekt aufgenommen wird.
// webpack.config.js — full tree shaking configuration
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
module.exports = {
mode: "production", // enables tree shaking + terser minification
optimization: {
usedExports: true, // marks unused exports for removal
sideEffects: true, // respects package.json "sideEffects" field
concatenateModules: true, // scope hoisting — reduces module overhead
minimize: true, // terser removes marked unused exports
},
plugins: [
// Generate interactive bundle visualization
new BundleAnalyzerPlugin({
analyzerMode: "static", // creates report.html — no server needed
openAnalyzer: false,
}),
],
};
// package.json of a tree-shaking-friendly library
// {
// "name": "my-utils",
// "main": "./dist/index.cjs", // CJS for Node.js require()
// "module": "./dist/index.mjs", // ESM for bundlers (tree shaking)
// "exports": {
// ".": {
// "import": "./dist/index.mjs", // ESM — preferred by bundlers
// "require": "./dist/index.cjs" // CJS — for legacy Node.js
// }
// },
// "sideEffects": false // no modules have import side effects
// }
// Mark a specific function call as pure (no side effects)
// so bundler can remove it if result is unused
const VERSION = /*#__PURE__*/ computeVersion();
9. Scope Hoisting: der Partner von Tree Shaking
Scope Hoisting — in Webpack als Module Concatenation bekannt — ist die natürliche Ergänzung zu Tree Shaking. Ohne Scope Hoisting wrapt ein Bundler jedes Modul in eine eigene Funktion, um den Scope zu isolieren. Das erzeugt erheblichen Overhead: Tausende kleiner Wrapper-Funktionen im Bundle, jede mit ihrem eigenen Scope-Setup. Mit Scope Hoisting fasst der Bundler Module, die statisch verknüpft sind und keine zyklischen Abhängigkeiten haben, in einem einzigen Scope zusammen — weniger Funktionen, weniger Overhead, kleineres Bundle, schnellere Ausführung.
Scope Hoisting setzt, wie Tree Shaking, ESM voraus. CJS-Module müssen separat gewrapped bleiben, weil ihre dynamische Natur eine statische Fusion unmöglich macht. In Webpack ist Scope Hoisting mit optimization.concatenateModules: true aktiv (Standard in Production-Mode). Rollup macht Scope Hoisting immer — es ist ein fundamentales Designziel von Rollup, nicht eine optionale Optimierung. Das ist einer der Hauptgründe, warum Rollup-Bundles für Libraries so effizient sind: kein Modul-Wrapper-Overhead, kein duplizierter Scope-Setup-Code.
10. Zusammenfassung
Tree Shaking ist eine der effektivsten Performance-Optimierungen für JavaScript-Anwendungen — aber nur, wenn die Voraussetzungen stimmen. ES-Module sind zwingend notwendig, da nur sie die statische Import-Export-Analyse ermöglichen, die Bundler für Tree Shaking benötigen. Das sideEffects-Feld in package.json ist für Bibliotheks-Autoren Pflicht — ohne es verhindert der Bundler vorsichtshalber das Entfernen von Modulen. Named Exports statt Default-Export-Objekte, keine Klassen mit statischen Methoden und der /*#__PURE__*/-Kommentar für seiteneffektfreie Funktionsaufrufe sind die Code-Patterns, die Tree Shaking auf Funktionsebene ermöglichen.
Mit Bundle-Analyse-Tools wie webpack-bundle-analyzer und rollup-plugin-visualizer lässt sich messen, ob Tree Shaking tatsächlich greift und welche Bibliotheken ungenutzten Code ins Bundle bringen. Das Ergebnis konsequenten Tree Shakings ist messbar: Bundles, die nur den tatsächlich verwendeten Code enthalten, laden schneller, parsen schneller und führen schneller aus — besonders auf mobilen Geräten, wo diese Unterschiede in direktem Zusammenhang mit Nutzererfahrung und Conversion-Rate stehen.
Mironsoft
JavaScript-Performance, Bundle-Optimierung und Build-Pipeline-Modernisierung
Bundle zu groß? Wir analysieren und optimieren.
Wir analysieren euren Build-Output, identifizieren Bibliotheken ohne Tree-Shaking-Support und optimieren die Build-Konfiguration für maximale Dead-Code-Elimination.
Bundle-Analyse
Vollständige Analyse des Build-Outputs — tote Code identifizieren, Bibliotheken ohne Tree-Shaking aufspüren
Build-Optimierung
Webpack/Vite-Konfiguration für maximales Tree Shaking, Scope Hoisting und Code-Splitting
Library-Design
Tree-Shaking-freundliche API-Gestaltung, sideEffects-Konfiguration und Dual-Publish-Setup
JavaScript Tree Shaking — Das Wichtigste auf einen Blick
ESM ist Pflicht
Tree Shaking funktioniert nur mit ES-Modulen. CJS (require) und IIFE erlauben keine statische Import-Analyse — Bundler müssen alles einschließen.
sideEffects-Flag
"sideEffects": false in package.json signalisiert: Alle Module sicher entfernbar. Array für Ausnahmen (CSS, Polyfills). Pflicht für jede Bibliothek.
Named Exports bevorzugen
export function foo() statt export default { foo }. Klassen mit statischen Methoden blockieren Tree Shaking. /*#__PURE__*/ für seiteneffektfreie Calls.
Analyse-Tools
webpack-bundle-analyzer: Treemap des Bundle-Inhalts. rollup-plugin-visualizer für Vite. bundlephobia.com: Tree-Shaking-Support vor npm install prüfen.