</>
{ }
React · Lazy Loading · Code Splitting · Performance · Bundle
React Lazy Loading & Code Splitting
an der richtigen Stelle einsetzen

Code Splitting ist keine einmalige Maßnahme, sondern eine kontinuierliche Strategie. React.lazy und dynamische Imports laden JavaScript nur dann, wenn es wirklich gebraucht wird – aber nur, wenn die Split-Grenzen an den richtigen Stellen gezogen werden. Zu viele kleine Chunks verlangsamen das Laden durch HTTP-Overhead. Zu wenige verhindern jeglichen Nutzen. Dieses Tutorial zeigt, wie man die Balance findet.

13 Min. Lesezeit React.lazy · Suspense · dynamische Imports · Bundle-Analyse · Preloading React 18 · Vite · Webpack 5 · TypeScript

1. Warum Code Splitting und wo es wirkt

React Lazy Loading und Code Splitting lösen ein konkretes Performance-Problem: Ohne Splitting lädt der Browser beim ersten Aufruf einer Seite das gesamte JavaScript-Bundle – unabhängig davon, welche Teile der Applikation der Nutzer tatsächlich besucht. Bei einer größeren Single-Page-Application mit vielen Seiten, Dashboards, Admin-Bereichen und schweren Libraries bedeutet das oft mehrere Megabyte JavaScript, die vor dem ersten Paint geparst und ausgeführt werden müssen. Code Splitting teilt dieses Bundle in kleinere Chunks auf und lädt sie bei Bedarf.

Die Metrik, die Code Splitting direkt verbessert, ist Time to Interactive (TTI): die Zeit, bis die Seite vollständig interaktiv ist. Nicht alle Metriken werden gleich beeinflusst – Largest Contentful Paint (LCP) und First Contentful Paint (FCP) verbessern sich nur, wenn das initiale Bundle signifikant kleiner wird. Das setzt voraus, dass die Grenzen des Splittings an den richtigen Stellen gezogen werden. Routen sind die wichtigste und wirksamste Split-Grenze. Einzelne Komponenten zu splitten bringt nur dann etwas, wenn sie schwere Abhängigkeiten mitbringen, die nicht auf allen Seiten benötigt werden.

2. Bundle-Analyse: erst messen, dann splitten

Bevor Code Splitting eingeführt wird, muss das aktuelle Bundle analysiert werden. Ohne Messung fehlt die Grundlage, um zu entscheiden, wo gesplittet werden soll und wie viel Verbesserung zu erwarten ist. Das wichtigste Tool: rollup-plugin-visualizer für Vite oder webpack-bundle-analyzer für Webpack. Beide erzeugen interaktive Treemaps, die zeigen, welche Module wie viel Platz im Bundle einnehmen. Überraschend oft sind es nicht die eigenen Komponenten, sondern Dependencies: Moment.js, Chart.js, PDF-Libraries, Editor-Bibliotheken oder Datepicker.

Ein zweites wertvolles Analysewerkzeug ist der Coverage-Tab in Chrome DevTools. Er zeigt, wie viel des geladenen JavaScripts beim ersten Seitenaufruf tatsächlich ausgeführt wird. Ungenutzte Bereiche sind Kandidaten für Code Splitting. Die Kombination beider Analysen ergibt ein klares Bild: welche Modules groß sind (Bundle Analyzer) und welche davon nicht sofort benötigt werden (Coverage). Diese Schnittmenge ist der Ausgangspunkt für gezielte React Lazy Loading-Maßnahmen.


// vite.config.ts — bundle analyzer setup
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    react(),
    // Generates stats.html — open in browser to see treemap
    visualizer({
      filename: 'dist/stats.html',
      open: true,          // auto-open after build
      gzipSize: true,      // show gzip size (closer to network transfer)
      brotliSize: true,    // show brotli size
      template: 'treemap', // options: treemap | sunburst | network
    }),
  ],
  build: {
    rollupOptions: {
      output: {
        // Manual chunking: keep large stable dependencies in separate chunks
        manualChunks: {
          'vendor-react': ['react', 'react-dom', 'react-router-dom'],
          'vendor-charts': ['recharts'],       // loaded only on chart pages
          'vendor-editor': ['@tiptap/react'],  // loaded only in editor
        },
      },
    },
  },
});

3. React.lazy: Komponenten on demand laden

React.lazy nimmt eine Funktion entgegen, die ein dynamisches import() zurückgibt, und gibt eine lazy-geladene Komponente zurück. Der dynamische Import erzeugt einen eigenen Chunk, der erst geladen wird, wenn die Komponente zum ersten Mal gerendert werden soll. React.lazy unterstützt nur Default-Exports: Die importierte Datei muss die Komponente als Default-Export bereitstellen. Named Exports müssen in eine separate Datei ausgelagert oder mit einem Re-Export-Wrapper versehen werden.

Ein wichtiges Detail: React.lazy und der dynamische Import lösen das Laden des Chunks aus, sobald die Komponente gerendert werden soll – nicht früher. Das bedeutet, dass beim ersten Rendern der lazy-geladenen Komponente ein Netzwerkrequest ausgeführt wird, der eine wahrnehmbare Latenz verursachen kann. Für Routen-Splitting auf einer schnellen Verbindung ist das akzeptabel. Für Komponenten, die nach einer Nutzerinteraktion erscheinen, kann Preloading dieses Problem entschärfen. Suspense ist dabei immer zwingend erforderlich – React rendert den Fallback während des Ladens des Chunks.

4. Suspense-Grenzen strategisch platzieren

Die Platzierung von Suspense-Grenzen bestimmt, welcher Teil der UI während des Ladens eines Chunks ausgetauscht wird. Zu hoch platzierte Suspense-Grenzen – etwa auf der Root-Ebene – ersetzen die gesamte Seite mit einem Ladebalken, wenn irgendeine lazy-geladene Komponente lädt. Zu tief platzierte Grenzen erzeugen viele kleine, unkoordinierte Ladezustände. Die beste Strategie: Suspense-Grenzen auf Routenebene und für große, eigenständige Komponentenbereiche wie Modals, Dashboards-Widgets oder Sidebars.

React 18 und Concurrent Mode verbessern das Suspense-Verhalten: Mit useTransition kann das Laden eines neuen Routen-Chunks die aktuelle Seite sichtbar halten, bis der neue Chunk geladen ist – kein Layout-Flash durch einen Ladebalken zwischen zwei Seiteninhalten. Das ist das „Concurrent Rendering with Suspense"-Muster aus dem vorherigen Artikel. Für den Fallback selbst gilt: Skeleton-Layouts, die der Form des tatsächlichen Inhalts entsprechen, sind besser als generische Spinner, weil sie Layout-Shifts nach dem Laden reduzieren.


import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

// Route-level lazy loading — each route is a separate chunk
const HomePage = lazy(() => import('./pages/HomePage'));
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
const ReportsPage = lazy(() => import('./pages/ReportsPage'));
const AdminPage = lazy(() => import('./pages/AdminPage'));

// Heavy component only needed on specific interaction
const PDFExportModal = lazy(() =>
  import('./components/PDFExportModal').then(module => ({
    // Re-export named export as default — React.lazy requires default export
    default: module.PDFExportModal,
  }))
);

function App() {
  return (
    // Route-level Suspense: full-page skeleton during navigation
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/dashboard" element={<DashboardPage />} />
        <Route path="/reports" element={<ReportsPage />} />
        <Route path="/admin" element={<AdminPage />} />
      </Routes>
    </Suspense>
  );
}

// Component-level Suspense: isolated loading for heavy modal
function ReportsPage() {
  const [showExport, setShowExport] = useState(false);

  return (
    <div>
      <ReportTable />
      <button onClick={() => setShowExport(true)}>Als PDF exportieren</button>
      {showExport && (
        // PDF chunk only loads when user explicitly requests export
        <Suspense fallback={<ModalSkeleton />}>
          <PDFExportModal onClose={() => setShowExport(false)} />
        </Suspense>
      )}
    </div>
  );
}

5. Routen-basiertes Splitting: die wichtigste Split-Grenze

Routen-basiertes Code Splitting ist die wichtigste und wirksamste Anwendung von React Lazy Loading. Jede Route wird zu einem eigenständigen Chunk: Der Nutzer lädt beim ersten Besuch nur den Code der aktuellen Seite, nicht den aller anderen Seiten der Applikation. Für eine Applikation mit einem Admin-Bereich, mehreren Dashboards und einem öffentlichen Bereich bedeutet das, dass ein Besucher des öffentlichen Bereichs niemals den Admin-JavaScript-Code lädt. Das reduziert das initiale Bundle je nach Applikation um 30–70 %.

In React Router v6 ist routen-basiertes Splitting durch React.lazy einfach umzusetzen: Jede Routen-Komponente wird über einen dynamischen Import geladen. In Next.js App Router ist dieses Splitting automatisch – jede page.tsx-Datei wird automatisch in einen eigenen Chunk aufgeteilt. Für sehr große Seiten-Komponenten kann innerhalb einer Route weiter gesplittet werden, etwa indem schwere Tab-Inhalte erst geladen werden, wenn der Tab aktiviert wird. Das Muster: State kontrolliert die Sichtbarkeit, Suspense und lazy control den Chunk.

6. Komponenten-Splitting: wann es sich lohnt

Nicht jede Komponente lohnt sich als Split-Grenze. Kleine Komponenten mit wenig JavaScript zu splitten führt zu mehr HTTP-Requests für minimalen Chunk-Größen-Gewinn. Die Faustregel: Komponenten-Splitting lohnt sich, wenn die Komponente schwere Abhängigkeiten hat, die von anderen Seiten nicht benötigt werden, und wenn sie nicht sofort beim ersten Seitenaufruf sichtbar ist. Gute Kandidaten sind: Rich-Text-Editoren (Tiptap, Quill, Slate – oft 200+ kB), Chart-Bibliotheken (Recharts, Chart.js – 100+ kB), PDF-Viewer, Code-Syntax-Highlighter (Prism, Highlight.js) und Karten (Leaflet, Mapbox GL).

Schlechte Kandidaten für Komponenten-Splitting: Navigationskomponenten, die auf jeder Seite erscheinen. Formulare mit wenigen Feldern ohne schwere Abhängigkeiten. Modal-Dialoge, die sofort nach der ersten Nutzerinteraktion erscheinen sollen, ohne wahrnehmbare Latenz zu erlauben. Für diese Fälle ist Preloading die bessere Strategie: Den Chunk schon laden, wenn der Nutzer mit dem Mauszeiger auf den Auslöser zeigt – dann ist er bereits im Cache, wenn die Komponente tatsächlich gerendert wird.

7. Preloading: Chunks vor dem Bedarf laden

Preloading löst das Latenz-Problem beim ersten Rendern eines Lazy-Chunks. Statt den Chunk erst zu laden, wenn er gerendert werden soll, wird er beim Hover über einen Button oder beim Idle-State des Browsers vorab geladen. Das bedeutet, dass der Chunk beim tatsächlichen Klick bereits im Browser-Cache liegt und sofort verfügbar ist – wahrgenommene Ladezeit nahe null. Das Muster: import() beim Hover-Event aufrufen, ohne das Ergebnis zu verwenden. Der Browser lädt und cached den Chunk, React.lazy kann ihn beim nächsten Render sofort aus dem Cache lesen.

Eine sauberere Alternative ist die Nutzung von link rel="prefetch" oder link rel="preload"-Tags, die der Browser eigenständig mit niedrigster Priorität im Hintergrund laden kann. Webpack und Vite unterstützen Magic Comments für diese Hints: import(/* webpackPrefetch: true */ './HeavyComponent') oder import(/* webpackPreload: true */ './HeavyComponent'). Prefetch lädt den Chunk im Idle-State, Preload lädt ihn gleichzeitig mit dem aktuellen Chunk – Preload ist nur sinnvoll, wenn der Chunk mit hoher Wahrscheinlichkeit sofort benötigt wird.


import { lazy, Suspense, useCallback } from 'react';

// Lazy load heavy chart component
const ChartDashboard = lazy(() =>
  import(
    /* webpackChunkName: "chart-dashboard" */
    /* webpackPrefetch: true */
    './ChartDashboard'
  )
);

// Preload pattern: trigger import on hover, render on click
const preloadChart = () => import('./ChartDashboard');

function ReportButton() {
  const [showChart, setShowChart] = useState(false);

  return (
    <>
      <button
        // Preload chunk on hover — by click it's already in cache
        onMouseEnter={preloadChart}
        onFocus={preloadChart}        // keyboard navigation support
        onClick={() => setShowChart(true)}
      >
        Bericht anzeigen
      </button>

      {showChart && (
        <Suspense fallback={<ChartSkeleton />}>
          <ChartDashboard />
        </Suspense>
      )}
    </>
  );
}

// Idle-time preloading: load chunks when browser has nothing else to do
function useIdlePreload(importFn: () => Promise<unknown>) {
  useEffect(() => {
    const id = requestIdleCallback(
      () => { importFn(); },  // browser calls this when idle
      { timeout: 3000 }       // force after 3 seconds even if not idle
    );
    return () => cancelIdleCallback(id);
  }, [importFn]);
}

8. Typische Fehler beim Code Splitting

Der häufigste Fehler: zu granulares Splitting. Wenn 50 kleine Komponenten jeweils in eigene Chunks gesplittet werden, entstehen 50 separate HTTP-Requests – auf HTTP/2 besser als auf HTTP/1.1, aber immer noch overhead durch DNS-Lookup, TCP-Handshake, TLS und Request-Overhead. Der Nettovorteil kleiner Chunks wird durch diesen Overhead schnell aufgehoben. Die Mindestgröße für einen sinnvollen Split liegt bei etwa 30–50 kB gzip – darunter ist das Verhältnis von Overhead zu Ersparnis ungünstig.

Ein zweiter Fehler: Keine Error-Boundaries um Suspense-Grenzen. Wenn ein Chunk-Ladefehler auftritt – etwa weil der Nutzer eine veraltete URL aufgerufen hat und der Chunk-Dateiname sich durch ein neues Build geändert hat – wirft React einen Fehler. Ohne Error-Boundary stürzt die gesamte Applikation ab und zeigt eine leere Seite. Mit einer Error-Boundary wird der Fehler abgefangen und eine sinnvolle Fehlermeldung mit Reload-Button angezeigt. Jede Suspense-Grenze sollte von einer Error-Boundary umschlossen sein.

9. Splitting-Strategien im Vergleich

Verschiedene Split-Strategien haben unterschiedliche Auswirkungen auf Performance, Entwicklererfahrung und Wartbarkeit. Der Vergleich hilft, die richtige Strategie für den jeweiligen Kontext zu wählen.

Strategie Wirkung Aufwand Empfehlung
Routen-Splitting Sehr hoch (30–70 % kleiner) Gering (React.lazy + Suspense) Immer als Erstes
Schwere Libraries Hoch (große Dependencies) Mittel (dynamic import) Bei Libraries > 50 kB
Modals / Overlays Mittel (nur bei schweren Deps) Gering Mit Preloading kombinieren
manualChunks (Vendor) Caching (stable hash) Mittel (Build-Konfiguration) Für React, Router, Utility-Libs
Micro-Splitting (< 10 kB) Negativ (HTTP-Overhead) Hoch (Wartungsaufwand) Vermeiden

Die optimale Chunk-Größe liegt bei 50–150 kB gzip für initiale Chunks und 20–100 kB für Lazy-Chunks. Initiale Chunks sollten stabil sein (same hash bei unveränderten Dependencies), damit Nutzer sie aus dem Browser-Cache laden können. Vendor-Splitting via manualChunks erreicht das: React, React-DOM und andere stabile Libraries bekommen einen eigenen Chunk mit stabilem Hash, der sich nur bei Library-Updates ändert.

Mironsoft

React Performance, Bundle-Optimierung und Code-Splitting-Strategie

React-Bundle analysieren und Code Splitting optimieren?

Wir analysieren euer React-Bundle mit Visualizer und Coverage-Tools, identifizieren die größten Optimierungspotenziale und implementieren eine Splitting-Strategie, die Time to Interactive messbar verbessert.

Bundle-Audit

Treemap-Analyse, Coverage-Report und Chunk-Strategie-Empfehlung

Implementation

Routen-Splitting, Vendor-Chunks und Preloading-Strategie einrichten

Messung

Lighthouse, Web Vitals und TTI vor/nach Optimierung vergleichen

10. Zusammenfassung

React Lazy Loading und Code Splitting sind mächtige Performance-Werkzeuge, die ihren Nutzen erst entfalten, wenn die Split-Grenzen an den richtigen Stellen gezogen werden. Die erste und wichtigste Maßnahme ist immer Routen-basiertes Splitting: Jede Route in einen eigenen Chunk, der nur bei Besuch geladen wird. Bundle-Analyse mit Visualizer und Browser-Coverage-Tool zeigt, welche Dependencies zusätzlich als Kandidaten für Splitting oder Vendor-Chunking in Frage kommen. Preloading auf Hover oder im Idle-State beseitigt wahrnehmbare Latenz beim ersten Rendern lazy-geladener Chunks.

Error-Boundaries um jede Suspense-Grenze sind Pflicht, damit Chunk-Ladefehler nicht zur leeren Seite führen. Micro-Splitting kleiner Komponenten vermeiden – der HTTP-Overhead überwiegt den Bundle-Größen-Vorteil. Vendor-Chunks für stabile Libraries wie React und React-DOM verbessern die Cache-Stabilität zwischen Deployments. Regelmäßige Bundle-Analyse nach jedem größeren Feature – nicht nur einmalig zu Beginn – hält die Performance-Konfiguration aktuell.

React Lazy Loading & Code Splitting — Das Wichtigste auf einen Blick

Erst messen

rollup-plugin-visualizer + Browser-Coverage-Tab. Größte, selten genutzte Module identifizieren. Dann splitten — nicht umgekehrt.

Routen zuerst

Routen-Splitting via React.lazy bringt 30–70 % kleinere initiale Bundles. Jede Route = ein Chunk. Next.js macht das automatisch.

Preloading

import() auf Hover/Focus aufrufen. Chunk ist im Cache, wenn der Nutzer klickt. webpackPrefetch für Idle-Preloading nutzen.

Error Boundaries

Jede Suspense-Grenze mit Error-Boundary umschließen. Chunk-Ladefehler führen sonst zur leeren Seite ohne Fehlermeldung.

11. FAQ: React Lazy Loading und Code Splitting

1React.lazy vs. dynamischer Import?
import() ist ein JS-Feature zum asynchronen Laden von Chunks. React.lazy ist ein React-Wrapper darum, der das Ergebnis als Komponente nutzbar macht. React.lazy erfordert immer Suspense.
2Code Splitting manuell in Next.js einrichten?
Nein. Next.js teilt jede page.tsx automatisch auf. Für weiteres Splitting: next/dynamic verwenden — unterstützt zusätzlich ssr: false für Client-only-Chunks.
3Was passiert wenn ein Chunk nicht lädt?
React wirft einen Fehler. Ohne Error-Boundary: leere Seite. Mit Error-Boundary um die Suspense-Grenze: Fehlermeldung mit Reload-Option. Immer Error-Boundary setzen.
4Minimale Chunk-Größe für sinnvolles Splitting?
30–50 kB gzip. Darunter überwiegen HTTP-Request-Overhead den Größenvorteil. Micro-Splitting kleiner Komponenten verschlechtert Performance.
5React.lazy mit Named Exports?
Nicht direkt — Default-Export erforderlich. Workaround: import('./M').then(m => ({ default: m.Named })). Oder eigene Re-Export-Datei mit export default.
6Was ist webpackPrefetch?
Magic Comment, das einen link rel='prefetch' erzeugt. Browser lädt Chunk im Idle-State mit niedrigster Priorität. Für bald benötigte, aber nicht sofortige Chunks.
7Prefetch vs. Preload?
Prefetch: niedrige Priorität, Idle-State — zukünftig benötigte Chunks. Preload: hohe Priorität, gleichzeitig mit aktuellem Bundle — nur für sofort benötigte Chunks.
8Verbessert Code Splitting LCP und FCP?
Nur wenn initiales Bundle signifikant kleiner wird. Routen-Splitting verbessert LCP/FCP. Komponenten-Splitting ohne Routen-Splitting kaum Effekt auf diese Metriken.
9Preloading auf Hover implementieren?
const preload = () => import('./HeavyComponent') als onMouseEnter und onFocus am Auslöser setzen. Browser lädt und cached Chunk — beim Klick sofort verfügbar.
10Was ist manualChunks?
Vite/Rollup-Konfiguration zum Gruppieren von Modulen in eigene Chunks. Für stabile Vendor-Libraries (React, React-DOM): Hash ändert sich nur bei Library-Updates — bessere Cache-Stabilität.