Code-Splitting-Strategien für schnellere Webanwendungen
Praktische Code-Splitting-Techniken mit Webpack und dynamischen Imports, um die anfängliche Bundle-Größe zu senken und die Ladezeit zu verbessern.

Ein 2MB großes JavaScript-Bundle auszuliefern bedeutet, dass jeder Nutzer die vollen Kosten im Voraus zahlt — selbst wenn er nur eine Seite besucht. Code Splitting zerlegt deine Anwendung in kleinere Chunks, die bei Bedarf geladen werden, macht den initialen Ladevorgang schnell und verschiebt den Rest, bis er tatsächlich gebraucht wird.
Das Konzept ist einfach. Die Umsetzung hat ihre Tücken. Dieser Leitfaden behandelt die Techniken, die zuverlässig funktionieren, und die Muster, die subtile Performance-Regressionen verursachen.
Routenbasiertes Splitting: das Fundament
Das Splitting mit der größten Wirkung findet an den Routengrenzen statt. Jede Seite wird zu ihrem eigenen Chunk, der nur geladen wird, wenn der Nutzer zu ihr navigiert.
// ❌ Importing everything upfront — one massive bundle
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';
import Settings from './pages/Settings';
import Analytics from './pages/Analytics';
import AdminPanel from './pages/AdminPanel';
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/admin" element={<AdminPanel />} />
</Routes>
);
}// ✅ Lazy-loaded routes — each page in its own chunk
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/admin" element={<AdminPanel />} />
</Routes>
</Suspense>
);
}Beim routenbasierten Splitting lädt ein Nutzer, der die Startseite besucht, nur den Home-Chunk herunter. Die Chunks für Dashboard, Einstellungen und Admin werden bei der Navigation geladen. Bei Anwendungen mit zehn oder mehr Routen kann dies allein das initiale Bundle um 60-80% verkleinern.
Dynamische Imports für schwere Bibliotheken
Große Bibliotheken, die in bestimmten Features verwendet werden, gehören nicht ins Haupt-Bundle. Dynamisches import() verschiebt sie, bis das Feature ausgelöst wird.
// ❌ Chart library loaded for every user, even those who never view charts
import { Chart } from 'chart.js';
import { marked } from 'marked';
import hljs from 'highlight.js';
export function renderAnalytics(data: AnalyticsData) {
const chart = new Chart(canvas, { type: 'line', data });
return chart;
}// ✅ Heavy libraries loaded only when the feature is used
export async function renderAnalytics(data: AnalyticsData) {
const { Chart } = await import('chart.js');
const chart = new Chart(canvas, { type: 'line', data });
return chart;
}
export async function renderMarkdown(content: string) {
const [{ marked }, hljs] = await Promise.all([
import('marked'),
import('highlight.js'),
]);
marked.setOptions({
highlight: (code, lang) => hljs.highlight(code, { language: lang }).value,
});
return marked(content);
}Promise.all lädt mehrere Bibliotheken parallel, wenn sie immer zusammen verwendet werden. Das vermeidet sequentielles Waterfall-Loading.
Webpack Magic Comments
Webpack bietet Magic Comments, um Chunk-Namen, Ladestrategie und Prefetching zu steuern. Sie sind entscheidend für die Feinabstimmung des Ladeverhaltens.
// Named chunks — easier debugging and cache management
const Editor = lazy(() =>
import(/* webpackChunkName: "editor" */ './components/Editor')
);
// Prefetch — load in background after main resources finish
const AdminPanel = lazy(() =>
import(
/* webpackChunkName: "admin" */
/* webpackPrefetch: true */
'./pages/AdminPanel'
)
);
// Preload — load in parallel with the current navigation
const CriticalWidget = lazy(() =>
import(
/* webpackChunkName: "critical-widget" */
/* webpackPreload: true */
'./components/CriticalWidget'
)
);Der Unterschied zwischen Prefetch und Preload ist wichtig:
<!-- Prefetch: downloaded during idle time, low priority -->
<!-- Browser fetches this AFTER the current page finishes loading -->
<link rel="prefetch" href="/static/js/admin.chunk.js" />
<!-- Preload: downloaded immediately, high priority -->
<!-- Browser fetches this IN PARALLEL with the current page -->
<link rel="preload" href="/static/js/critical-widget.chunk.js" as="script" />Verwende Prefetch für Routen, die der Nutzer als Nächstes wahrscheinlich besuchen wird. Verwende Preload für Komponenten, die auf der aktuellen Seite gerendert werden, aber aus Caching-Gründen aufgeteilt wurden.
Vendor-Chunk-Strategie
Das Trennen von Vendor-Code und Anwendungscode verbessert das Caching. Dein Anwendungscode ändert sich häufig, aber react, lodash und date-fns nur selten. Sie zu trennen bedeutet, dass Nutzer nur das erneut herunterladen, was sich geändert hat.
// webpack.config.js — vendor splitting configuration
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// Core framework — changes very rarely
framework: {
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
name: 'framework',
priority: 40,
enforce: true,
},
// Large libraries — split individually for granular caching
chartjs: {
test: /[\\/]node_modules[\\/]chart\.js[\\/]/,
name: 'chartjs',
priority: 30,
},
// Remaining vendor code
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 20,
minSize: 30000,
},
// Shared application code used by 2+ chunks
commons: {
minChunks: 2,
name: 'commons',
priority: 10,
reuseExistingChunk: true,
},
},
},
},
};Das erzeugt eine Hierarchie: Framework-Chunk (monatelang gecacht), Chunks großer Bibliotheken (individuell gecacht), allgemeiner Vendor-Chunk (gecacht, bis sich eine Abhängigkeit aktualisiert) und ein Commons-Chunk für geteilten Anwendungscode.
Splitting auf Komponentenebene
Für Komponenten hinter einer Nutzerinteraktion — Modals, Dropdowns oder komplexe Formulare — teile auf Komponentenebene statt auf Routenebene.
import { lazy, Suspense, useState } from 'react';
// Heavy modal with rich text editor, only loaded when opened
const RichTextModal = lazy(() =>
import(
/* webpackChunkName: "rich-text-modal" */
/* webpackPrefetch: true */
'./components/RichTextModal'
)
);
function DocumentPage() {
const [showEditor, setShowEditor] = useState(false);
return (
<div>
<h2>Document Viewer</h2>
<DocumentContent />
<button onClick={() => setShowEditor(true)}>
Edit Document
</button>
{showEditor && (
<Suspense fallback={<ModalSkeleton />}>
<RichTextModal onClose={() => setShowEditor(false)} />
</Suspense>
)}
</div>
);
}Der Hinweis webpackPrefetch: true weist den Browser an, den Modal-Chunk während der Leerlaufzeit zu laden. Wenn der Nutzer auf "Edit Document" klickt, ist der Chunk wahrscheinlich bereits gecacht, und das Modal erscheint sofort.
Splits analysieren und messen
Splitting ohne Messung führt zu Over-Splitting oder versehentlicher Duplizierung. Nutze Bundle-Analyse, um zu überprüfen, ob deine Strategie wie beabsichtigt funktioniert.
# Install the analyzer
npm install --save-dev webpack-bundle-analyzer
# Generate stats and visualize
npx webpack --profile --json > stats.json
npx webpack-bundle-analyzer stats.json
# Or add to webpack config for automatic analysis
# const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
# plugins: [new BundleAnalyzerPlugin()]// Runtime check: log chunk loading for debugging
if (process.env.NODE_ENV === 'development') {
const originalFetch = window.fetch;
window.fetch = function (...args) {
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
if (url?.includes('.chunk.js')) {
console.log(`[Chunk loaded] ${url}`);
}
return originalFetch.apply(this, args);
};
}Wichtige Metriken, die nach dem Splitting zu verfolgen sind:
- Initiale JS-Größe: Sollte für die meisten Anwendungen unter 200KB gzip-komprimiert liegen
- Largest Contentful Paint: Sollte sich mit richtigem Splitting verbessern
- Ungenutztes JavaScript: Der Coverage-Tab in den Chrome DevTools zeigt, wie viel Code pro Seite ungenutzt bleibt
Häufige Fehler
Over-Splitting erzeugt zu viele Netzwerk-Anfragen. Jeder Chunk hat HTTP-Overhead — Header, Verbindungsaufbau, Parsing. Ein 5KB-Utility in einen eigenen Chunk auszulagern, verschlechtert die Performance, statt sie zu verbessern.
Under-Splitting entsteht, wenn geteilte Abhängigkeiten über Chunks hinweg dupliziert werden. Zwei Routen-Chunks, die beide eine 100KB große Chart-Bibliothek importieren, bedeuten ohne richtige splitChunks-Konfiguration, dass der Nutzer sie zweimal herunterlädt.
Vergessene Error Boundaries um Lazy-Komponenten bedeuten, dass ein fehlgeschlagener Chunk-Load die Anwendung abstürzen lässt, statt eine Wiederholungsoption anzuzeigen. Umhülle Suspense-Grenzen in der Produktion immer mit Error Boundaries.
Die wichtigsten Erkenntnisse
- Beginne mit Splitting auf Routenebene — es liefert die größte Wirkung bei geringstem Aufwand
- Importiere schwere Bibliotheken dynamisch —
chart.js,monaco-editorund Ähnliches gehören nie ins Haupt-Bundle - Nutze Prefetch für wahrscheinliche nächste Seiten — Laden im Hintergrund eliminiert Navigationsverzögerungen
- Konfiguriere Vendor-Splitting fürs Caching — trenne Framework, große Bibliotheken und Anwendungscode
- Miss mit Bundle-Analyse — Splitting ohne Daten führt zu schlechteren Ergebnissen als gar kein Splitting
- Vermeide Micro-Splitting — Chunks unter 30KB erzeugen mehr Overhead, als sie einsparen


