Bundles verstehen, optimieren, dauerhaft kontrollieren
Ein 2-MB-JavaScript-Bundle ist keine Seltenheit, aber es ist fast immer vermeidbar. Bundle-Analyse mit Rollup und Vite macht unsichtbare Probleme sichtbar: Tree-Shaking-Lücken, doppelte Dependencies, unnötige Polyfills – und liefert die Grundlage für gezielte Optimierung.
Inhaltsverzeichnis
- 1. Warum Bundle-Analyse entscheidend ist
- 2. rollup-plugin-visualizer einrichten
- 3. Bundle-Analyse in Vite-Projekten
- 4. Tree-Shaking-Probleme erkennen und beheben
- 5. Code-Splitting: Chunks sinnvoll aufteilen
- 6. Doppelte Dependencies finden
- 7. Dynamische Imports und Lazy Loading
- 8. Bundle-Budget in CI durchsetzen
- 9. Rollup vs. Vite Bundle-Output im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Bundle-Analyse entscheidend ist
JavaScript-Bundle-Größe ist einer der direktesten Einflussfaktoren auf die Ladezeit einer Web-Anwendung. Jedes Kilobyte JavaScript muss heruntergeladen, geparst und kompiliert werden, bevor die erste Interaktion möglich ist. Lighthouse und Core Web Vitals messen diese Zeit direkt: Time to Interactive (TTI) und Total Blocking Time (TBT) sinken proportional zur eingesparten Bundle-Größe. Eine Bundle-Analyse mit Rollup oder Vite zeigt, welche Teile des Bundles tatsächlich wertvoll sind und welche unnötigerweise mitgeschleppt werden.
Das überraschende Ergebnis einer ersten Bundle-Analyse in einem gewachsenen Projekt: Oft machen wenige große Pakete den Großteil des Bundles aus – Datumsbibliotheken wie Moment.js mit vollständigen Locale-Dateien, UI-Komponentenbibliotheken, die zu großen Teilen importiert aber kaum genutzt werden, oder Polyfills für Browser, die die Zielgruppe längst nicht mehr nutzt. Ohne visuelle Bundle-Analyse sind diese Probleme unsichtbar, weil npm ls und package.json nur die Pakete zeigen, nicht deren kompilierte Größe im tatsächlichen Bundle.
2. rollup-plugin-visualizer einrichten
Das rollup-plugin-visualizer ist das Standard-Tool für die Bundle-Analyse in Rollup-Projekten. Es generiert nach dem Build einen interaktiven Treemap-Report als HTML-Datei, der zeigt wie groß jedes Modul im finalen Bundle ist – in drei Darstellungen: treemap (verschachtelt nach Modul-Hierarchie), sunburst (radialer Kreis) und network (Abhängigkeitsgraph). Installiert wird es mit npm install -D rollup-plugin-visualizer und in die Rollup-Konfiguration eingetragen.
Der Visualizer bietet drei Größenmodi: stat (unkomprimierte Größe vor der Bundling-Verarbeitung), parsed (Größe nach dem Bundling, vor Minifikation) und gzip (simulierte Gzip-komprimierte Größe). Für Performance-Entscheidungen ist der gzip-Modus am relevantesten, weil er der tatsächlich übertragenen Größe am nächsten kommt. Der Unterschied kann erheblich sein: Ein lodash-chunk, der als parsed 50 KB groß erscheint, kann nach gzip nur 15 KB groß sein.
// rollup.config.js — Bundle analysis with rollup-plugin-visualizer
import { visualizer } from 'rollup-plugin-visualizer';
import { defineConfig } from 'rollup';
export default defineConfig({
input: 'src/main.js',
output: {
dir: 'dist',
format: 'es',
chunkFileNames: '[name]-[hash].js',
},
plugins: [
// Only run visualizer in analysis mode to avoid bloating normal builds
process.env.ANALYZE && visualizer({
filename: 'dist/bundle-report.html',
open: true, // auto-open browser after build
gzipSize: true, // show gzip-estimated sizes
brotliSize: true, // show brotli-estimated sizes
template: 'treemap', // treemap | sunburst | network | list | raw-data
}),
].filter(Boolean),
});
// Run analysis: ANALYZE=1 rollup -c
// Or add to package.json:
// "analyze": "ANALYZE=1 rollup -c"
3. Bundle-Analyse in Vite-Projekten
Vite basiert intern auf Rollup für Production-Builds, weshalb das rollup-plugin-visualizer direkt in der Vite-Konfiguration verwendet werden kann. Alternativ gibt es vite-bundle-visualizer als eigenständiges Tool: Es ruft intern vite build auf und erzeugt direkt einen Visualizer-Report, ohne die Vite-Konfiguration anfassen zu müssen. Das ist für schnelle Einmalanalysen ideal, ohne das Projekt dauerhaft anzupassen.
Vite bietet zusätzlich die eingebaute Option build.rollupOptions.output.manualChunks, mit der man gezielt bestimmt, welche Module in welchen Chunks landen. Ohne diese Konfiguration entscheidet Rollup autonom über die Chunk-Aufteilung, was bei großen Abhängigkeitsbäumen zu suboptimalen Ergebnissen führen kann. Eine typische Optimierung: Alle großen Third-Party-Bibliotheken in einen separaten vendor-Chunk packen, der sich seltener ändert und somit länger im Browser-Cache bleibt als der Anwendungs-Code.
// vite.config.js — Bundle analysis and chunk optimization
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
build: {
rollupOptions: {
output: {
// Manual chunk splitting for better caching
manualChunks(id) {
// Large UI framework in its own chunk
if (id.includes('node_modules/vue') || id.includes('node_modules/@vue')) {
return 'vue-vendor';
}
// Chart library (heavy) in dedicated chunk
if (id.includes('node_modules/chart.js') || id.includes('node_modules/echarts')) {
return 'charts';
}
// All other node_modules in vendor chunk
if (id.includes('node_modules')) {
return 'vendor';
}
},
// Deterministic chunk names for better cache invalidation
chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash][extname]',
},
},
},
plugins: [
process.env.ANALYZE === 'true' && visualizer({
filename: 'dist/bundle-analysis.html',
open: true,
gzipSize: true,
template: 'treemap',
}),
].filter(Boolean),
});
// ANALYZE=true vite build
4. Tree-Shaking-Probleme erkennen und beheben
Tree-Shaking – das automatische Entfernen von nicht genutztem Code durch Rollup – funktioniert nur unter bestimmten Bedingungen. Es funktioniert ausschließlich mit ES Modules (import/export), nicht mit CommonJS (require()). Es funktioniert nicht wenn ein Modul Seiteneffekte hat, die nicht deklariert sind. Es funktioniert nicht wenn man ein Objekt als Ganzes importiert statt einzelne named exports. Die Bundle-Analyse macht sichtbar, wann Tree-Shaking nicht greift: eine gesamte Bibliothek erscheint im Bundle, obwohl nur eine Funktion davon genutzt wird.
Ein häufiges Tree-Shaking-Problem in der Bundle-Analyse: Lodash. import _ from 'lodash' oder import { debounce } from 'lodash' schließt das gesamte Lodash-Bundle ein (~530 KB ungezipped), weil Lodash das Common JS-Format nutzt und kein Tree-Shaking ermöglicht. Die Lösung: import debounce from 'lodash/debounce' (direkter Dateipfad) oder der Wechsel zu lodash-es, der ES-Module-kompatibel ist. Eine andere häufige Quelle: import * as Icon from '@heroicons/react' – das importiert alle Heroicons statt nur die benötigten.
5. Code-Splitting: Chunks sinnvoll aufteilen
Code-Splitting ist die wichtigste Technik, um die initiale Bundle-Größe zu reduzieren. Statt einer einzelnen JavaScript-Datei erzeugt das Build-Tool mehrere Chunks, von denen der Browser beim ersten Laden nur den benötigten lädt. Rollup und Vite unterstützen zwei Arten von Code-Splitting: automatisches Splitting bei dynamischen import()-Aufrufen und manuelles Splitting via manualChunks-Konfiguration.
Die Bundle-Analyse hilft beim Erkennen von suboptimalem Code-Splitting: Wenn viele kleine Chunks entstehen (z.B. durch zu aggressives manuelles Splitting oder viele kleine dynamische Imports), entstehen unnötig viele HTTP-Requests und das Browser-Preloading wird ineffizient. Wenn ein einzelner Chunk sehr groß ist, blockiert er das Laden. Das Gleichgewicht: Third-Party-Bibliotheken in stabile, selten ändernde Chunks; Route-spezifischer Code in eigene Chunks für lazy Loading; gemeinsam genutzte Utilities in shared-Chunks, die von mehreren Routen parallel genutzt werden.
// Advanced manualChunks strategy based on bundle analysis findings
// Use after visualizer reveals inefficient chunk distribution
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// Route-level splitting — each route loads its own chunk
if (id.includes('/pages/dashboard/')) return 'page-dashboard';
if (id.includes('/pages/checkout/')) return 'page-checkout';
if (id.includes('/pages/admin/')) return 'page-admin';
// Heavy optional features — only loaded when needed
if (id.includes('node_modules/monaco-editor')) return 'monaco';
if (id.includes('node_modules/pdf-lib')) return 'pdf';
// Stable third-party: long-lived cache, rarely changes
if (id.includes('node_modules')) return 'vendor';
},
},
},
// Warn when any chunk exceeds 500 KB (before gzip)
chunkSizeWarningLimit: 500,
},
});
// After building, verify chunks with:
// ls -lh dist/assets/ | sort -k5 -h
// Large unexpected chunks = manualChunks needs adjustment
6. Doppelte Dependencies finden
Eine häufige Ursache für unnötige Bundle-Größe in der Bundle-Analyse: doppelte Dependencies. Das entsteht, wenn zwei Pakete verschiedene Versionen derselben Abhängigkeit mitbringen, die nicht dedupliziert werden können – weil die Versionen semantisch inkompatibel sind. Im Visualizer erscheint die Bibliothek dann mehrfach im Bundle, unter verschiedenen Chunk-Pfaden oder Versionsverzeichnissen. Ein typisches Beispiel: React in v17 und v18 gleichzeitig im Bundle, wenn eine alte Bibliothek noch auf React v17 zeigt.
Das Tool npm ls zeigt die vollständige Dependency-Baumstruktur und macht doppelte Versionen sichtbar. npm dedupe oder pnpm dedupe versucht, Duplikate aufzulösen. In Vite-Projekten kann man resolve.dedupe in der Konfiguration setzen, um sicherzustellen, dass ein Paket immer aus einer einzigen Quelle aufgelöst wird. Bei hartnäckigen Duplikaten – wenn eine Bibliothek explizit eine andere Version braucht – ist das Upgrade der problematischen Bibliothek meist die einzige saubere Lösung.
7. Dynamische Imports und Lazy Loading
Dynamische Imports (import('./modul.js')) sind die direkteste Methode, Code aus dem initialen Bundle zu verlagern. Rollup und Vite erkennen dynamische Imports und erstellen automatisch separate Chunks für jedes dynamisch importierte Modul. Der Browser lädt diese Chunks nur, wenn der dynamische Import tatsächlich aufgerufen wird – nicht beim initialen Laden der Seite.
In der Bundle-Analyse nach der Einführung dynamischer Imports ist es wichtig zu prüfen, ob die Chunks auch tatsächlich als separate Dateien erzeugt werden. Rollup kann unter bestimmten Umständen Chunks inlinen, wenn sie für zu klein oder zu eng verbunden mit dem Hauptchunk befunden werden. Das output.inlineDynamicImports-Flag muss auf false stehen (Default). Auch die Chunk-Namen sollten aussagekräftig sein: /* webpackChunkName: "feature-x" */ funktioniert nicht in Rollup/Vite – stattdessen verwendet man rollupOptions.output.chunkFileNames oder benennt das dynamisch importierte Modul entsprechend.
8. Bundle-Budget in CI durchsetzen
Die Bundle-Analyse ist am wertvollsten, wenn sie nicht nur einmalig manuell durchgeführt wird, sondern als automatisierter Schritt in der CI-Pipeline läuft. Mehrere Tools erlauben das Definieren eines Bundle-Budgets: Wenn der Build ein definiertes Limit überschreitet, schlägt die Pipeline fehl. Das verhindert, dass neue Dependencies oder unvorsichtige Imports das Bundle unbemerkt aufblähen.
Rollup schreibt Bundle-Statistiken in das bundle-Objekt des generateBundle-Hooks. Ein einfaches Skript kann nach dem Build die Chunk-Größen auslesen und mit definierten Limits vergleichen. Vite 5.x bietet build.chunkSizeWarningLimit als einfache Warnungsschwelle. Für präzisere Budget-Checks eignet sich das Tool bundlewatch, das über einen PR-Kommentar genau anzeigt um wie viel KB ein Bundle durch einen Commit gewachsen ist – ein starkes Signal für Code-Reviewer, die keine Build-Tools auf ihrer lokalen Maschine ausführen.
// ci-bundle-check.js — Fail CI if any chunk exceeds size limits
// Run after: vite build --reporter=json
import { readFileSync, readdirSync, statSync } from 'node:fs';
// Budget definitions (in bytes, before gzip)
const BUDGETS = {
'vendor': 300 * 1024, // 300 KB — stable third-party code
'main': 100 * 1024, // 100 KB — app entry point
'page-': 80 * 1024, // 80 KB per page chunk
default: 150 * 1024, // 150 KB fallback for all other chunks
};
const distDir = 'dist/assets';
const files = readdirSync(distDir).filter(f => f.endsWith('.js'));
let failed = false;
for (const file of files) {
const size = statSync(`${distDir}/${file}`).size;
// Find matching budget key
const budgetKey = Object.keys(BUDGETS).find(k => file.startsWith(k)) ?? 'default';
const limit = BUDGETS[budgetKey];
const kb = (size / 1024).toFixed(1);
const limitKb = (limit / 1024).toFixed(0);
if (size > limit) {
console.error(`BUDGET EXCEEDED: ${file} is ${kb}KB (limit: ${limitKb}KB)`);
failed = true;
} else {
console.log(`OK: ${file} — ${kb}KB / ${limitKb}KB`);
}
}
if (failed) process.exit(1);
9. Rollup vs. Vite Bundle-Output im Vergleich
Obwohl Vite intern Rollup für Production-Builds verwendet, unterscheiden sich die Out-of-the-Box-Ergebnisse. Vite hat eigene Defaults für Code-Splitting, Chunk-Naming und das Handling von CSS-in-JS, die sich von einem reinen Rollup-Setup unterscheiden. Die Bundle-Analyse beider Tools zeigt diese Unterschiede: Vite erzeugt standardmäßig agressiveres Code-Splitting mit mehr, kleineren Chunks; ein reines Rollup-Setup erzeugt standardmäßig weniger, größere Chunks.
| Eigenschaft | Rollup (direkt) | Vite (Rollup-basiert) | Empfehlung |
|---|---|---|---|
| Analyse-Tool | rollup-plugin-visualizer | visualizer oder vite-bundle-visualizer | Beide nutzen gleichen Visualizer |
| Standard-Splitting | Konservativ | Aggressiver (mehr Chunks) | manualChunks in beiden konfigurieren |
| Tree-Shaking | Vollständig konfigurierbar | Vollständig konfigurierbar | Gleich gut – beide nutzen Rollup |
| CSS-Handling | Plugin nötig | Eingebaut | Vite für Full-Stack-Apps |
| Build-Speed | Langsamer (JS-Bundler) | Schneller (Rolldown-Migration) | Vite für Developer Experience |
Die Bundle-Analyse ist in beiden Tools identisch aussagekräftig, weil der gleiche Visualizer verwendet wird. Der Workflow ist immer gleich: Build erzeugen, Visualizer-Report öffnen, größte Module identifizieren, Tree-Shaking-Probleme beheben, Code-Splitting anpassen, neu bauen, vergleichen. Das iterative Vorgehen – Build analysieren, optimieren, erneut analysieren – ist dabei wirksamer als jede einzelne Maßnahme allein.
Mironsoft
Frontend-Performance, Build-Optimierung und Core Web Vitals
JavaScript-Bundle zu groß und Ladezeit zu hoch?
Wir analysieren Ihr Bundle mit rollup-plugin-visualizer, identifizieren Tree-Shaking-Lücken, doppelte Dependencies und falsch konfiguriertes Code-Splitting – und liefern einen konkreten Optimierungsplan.
Bundle-Audit
Visualizer-Analyse, Tree-Shaking-Check, Duplicate-Detection und Chunk-Bewertung
Optimierung
manualChunks-Strategie, dynamische Imports, Bibliotheksmigration und Polyfill-Bereinigung
CI-Integration
Bundle-Budget in Pipeline einrichten, bundlewatch-Integration und PR-Kommentare für Größenänderungen
10. Zusammenfassung
Die Bundle-Analyse mit Rollup und Vite macht unsichtbare Bundle-Probleme sichtbar und ist der erste Schritt jeder substanziellen Performance-Optimierung. Das rollup-plugin-visualizer – in Rollup direkt, in Vite als Plugin – erzeugt interaktive Treemap-Reports, die zeigen welche Module wie viel Platz im Bundle belegen. Die drei häufigsten Findings: fehlgeschlagenes Tree-Shaking durch CommonJS-Dependencies, falsch konfiguriertes Code-Splitting das entweder zu wenige oder zu viele Chunks erzeugt, und doppelte Dependencies durch Versions-Konflikte.
Die Optimierungsstrategie folgt einem klaren Ablauf: Analysieren, größte Module identifizieren, Tree-Shaking durch korrekte Import-Syntax korrigieren, manualChunks für strategisches Code-Splitting konfigurieren, schwere Bibliotheken durch leichtere Alternativen ersetzen und dynamische Imports für wenig genutzte Features einführen. Das Bundle-Budget in CI – ein Skript, das den Build fehlschlägt wenn Limits überschritten werden – stellt sicher, dass diese Optimierungen nicht durch zukünftige Entwicklungen rückgängig gemacht werden. Bundle-Analyse ist keine einmalige Aufgabe, sondern ein kontinuierlicher Prozess.
Bundle-Analyse mit Rollup und Vite — Das Wichtigste auf einen Blick
Tool einrichten
npm install -D rollup-plugin-visualizer. In Rollup/Vite als Plugin. ANALYZE=1 als Env-Variable. gzipSize: true für realistische Größenangaben.
Tree-Shaking reparieren
import { debounce } from 'lodash/debounce' statt lodash als Ganzes. lodash-es statt lodash für ES-Module. Kein import * as wenn einzelne exports ausreichen.
Code-Splitting
manualChunks für vendor/app/page-Trennung. Dynamische import() für Routen und optionale Features. chunkSizeWarningLimit als Frühwarnsystem.
CI-Budget
Bundle-Größen nach Build prüfen und Pipeline bei Überschreitung fehlschlagen lassen. bundlewatch für PR-Kommentare mit Größenänderungen pro Commit.