Lade-Zustände elegant managen
Suspense ist weit mehr als ein Spinner-Wrapper für React.lazy. In React 18 und 19 koordiniert es parallele Datenfetches, verhindert Layout-Shifts und macht Lade-Zustände deklarativ — ohne einen einzigen isLoading-Boolean.
Inhaltsverzeichnis
- 1. Das Grundprinzip: Suspense als deklarativer Lade-Zustand
- 2. Suspense Boundaries richtig positionieren
- 3. Code-Splitting mit React.lazy
- 4. Datenfetch mit Suspense: use() in React 19
- 5. Render-Waterfalls erkennen und vermeiden
- 6. useTransition: Übergänge ohne Spinner-Flicker
- 7. Error Boundaries gemeinsam mit Suspense
- 8. Suspense und Server Components
- 9. Suspense-Patterns im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Das Grundprinzip: Suspense als deklarativer Lade-Zustand
React Suspense löst ein fundamentales Problem in der UI-Entwicklung: den imperativen Lade-Zustand. Klassisch verwalten Komponenten ihren Ladezustand mit einem isLoading-Boolean, einem data-State und einem error-State — in jedem useEffect neu. Das führt zu inkonsistenter Behandlung von Lade-Zuständen über die Anwendung hinweg und zu Race Conditions, wenn mehrere Anfragen gleichzeitig laufen. Suspense ersetzt dieses Muster durch eine deklarative Boundary: Eine Komponente zeigt an, dass sie noch auf Daten wartet, indem sie ein Promise wirft. React fängt dieses Promise und zeigt stattdessen den Fallback der nächsten Suspense-Boundary an.
Das Konzept ist elegant, weil der Lade-Zustand aus der Komponente selbst herausgezogen wird. Die Komponente beschreibt nur den Zustand, in dem alle Daten vorhanden sind — der Lade-Zustand ist eine orthogonale Concern, die von der Boundary-Hierarchie verwaltet wird. Das macht Komponenten einfacher, weil sie keinen Fehlerzustand oder Lade-Zustand kennen müssen, und die Boundary-Ebene kontrolliert, welche Teile der UI während des Ladens sichtbar bleiben.
2. Suspense Boundaries richtig positionieren
Die Positionierung von Suspense Boundaries ist einer der kritischsten Designentscheidungen in einer Suspense-basierten Architektur. Eine zu weit oben platzierte Boundary versteckt beim Laden die gesamte Seite hinter einem Spinner — das ist schlechter als das klassische isLoading-Pattern. Eine zu granulare Platzierung, eine Boundary pro Datenfetch, führt zu dem klassischen Waterfall-Problem: jede Boundary wartet auf ihre Daten, bevor die nächste Ebene starten kann.
Die goldene Regel für Suspense Boundaries: Sie sollen die sinnvolle UI-Einheit beschreiben, die als Ganzes sichtbar wird. Eine Produktseite könnte eine Boundary für den oberen Bereich (Hero, Preis, Hauptbild) und eine separate Boundary für den unteren Bereich (Bewertungen, ähnliche Produkte) haben. So laden diese Bereiche unabhängig — der obere Teil wird sofort sichtbar, sobald seine Daten da sind, ohne auf die Bewertungen zu warten. Das verbessert die wahrgenommene Performance erheblich.
// Suspense boundary placement — coarse vs. fine grained
import { Suspense, lazy } from 'react';
// WRONG: single top-level boundary hides everything during load
function ProductPage({ id }) {
return (
<Suspense fallback={<FullPageSpinner />}>
<ProductHero id={id} />
<ProductReviews id={id} /> {/* waits for both before showing anything */}
<SimilarProducts id={id} />
</Suspense>
);
}
// CORRECT: independent boundaries for independent data
function ProductPage({ id }) {
return (
<>
{/* Critical path: loads first, shown immediately */}
<Suspense fallback={<HeroSkeleton />}>
<ProductHero id={id} />
</Suspense>
{/* Below fold: loads in parallel, shown when ready */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews id={id} />
</Suspense>
<Suspense fallback={<ProductGridSkeleton />}>
<SimilarProducts id={id} />
</Suspense>
</>
);
}
3. Code-Splitting mit React.lazy
React.lazy ist die bekannteste Anwendung von Suspense und ermöglicht dynamischen Import von Komponenten, die erst bei Bedarf geladen werden. In großen Anwendungen kann Code-Splitting mit React.lazy den initialen Bundle-Footprint erheblich reduzieren: Modals, Drawers, Editor-Komponenten und administrative Bereiche müssen erst geladen werden, wenn der Nutzer sie tatsächlich öffnet. Das initiale Bundle enthält nur den kritischen Pfad.
Die Kombination von React.lazy mit Route-basiertem Code-Splitting ist der Standard in modernen React-Anwendungen. In React Router 6 und Next.js erfolgt das teilweise automatisch, aber manuelle Lazy-Boundaries an strategischen Stellen — etwa beim Öffnen eines Modals oder beim Aktivieren eines seltenen Features — können die Time-to-Interactive erheblich verbessern. Wichtig: Die Suspense-Boundary muss immer außerhalb des lazy-geladenen Components stehen, sonst gibt es eine Fehlermeldung.
4. Datenfetch mit Suspense: use() in React 19
React 19 führt den use()-Hook ein, der Promises und Context direkt in Komponenten auflöst und dabei die Suspense-Mechanik nutzt. Statt einen useEffect mit isLoading-State zu schreiben, übergibt man ein Promise direkt an use(): React suspendiert die Komponente, bis das Promise aufgelöst ist, und zeigt den Fallback der nächsten Boundary. Das ist der erste offizielle, nicht-experimentelle Mechanismus für Suspense-basiertes Datenladen ohne externe Bibliotheken.
In der Praxis nutzen die meisten Teams weiterhin React Query, SWR oder Apollo — diese Bibliotheken integrieren Suspense seit React 18 über den suspense: true-Parameter. Mit React Query v5 und useSuspenseQuery ist der Übergang zu Suspense-basiertem Fetching besonders einfach: Es braucht keine Änderung an der Abfragelogik, nur einen anderen Hook-Namen. Das Ergebnis ist eine Komponente ohne isLoading-Boolean, ohne optionalen data-Typ und ohne Race-Condition-Risiko.
// React 19: use() hook for Suspense-based data fetching
import { use, Suspense } from 'react';
import { useSuspenseQuery } from '@tanstack/react-query';
// React 19 use() — resolves a Promise with Suspense
function UserProfile({ userPromise }) {
// Suspends until promise resolves — no isLoading needed
const user = use(userPromise);
return <div>{user.name}</div>;
}
// Usage: create promise at route level (not in component to avoid re-fetch)
function UserPage({ id }) {
const userPromise = fetchUser(id); // created outside, stable reference
return (
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// React Query v5: useSuspenseQuery — no isLoading, data is always defined
function ProductDetails({ id }) {
const { data: product } = useSuspenseQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id),
});
// data is always Product, never undefined — Suspense handles loading state
return <h1>{product.name}</h1>;
}
5. Render-Waterfalls erkennen und vermeiden
Ein Render-Waterfall entsteht, wenn Komponenten sequentiell Daten laden: Komponente A suspendiert, bis ihre Daten geladen sind, rendert dann Komponente B, die ebenfalls suspendiert. Das Resultat ist ein Netzwerk-Wasserfall, bei dem jede Ebene erst startet, wenn die vorherige abgeschlossen ist — selbst wenn alle Requests unabhängig voneinander sind. Das ist das häufigste Performance-Problem in Suspense-basierten Anwendungen und entsteht durch eine zu tiefe Platzierung der Datenfetches.
Die Lösung heißt Fetch-Then-Render oder Render-As-You-Fetch: Alle Requests für eine Route werden gleichzeitig gestartet, bevor das Rendering beginnt. In React Query geschieht das mit prefetchQuery im Route-Loader. In Next.js werden parallele Fetches mit Promise.all in Server Components koordiniert. Der kritische Unterschied: Die Promises werden auf der Route-Ebene erstellt und als Props nach unten gereicht — nicht innerhalb jeder Komponente ausgelöst.
6. useTransition: Übergänge ohne Spinner-Flicker
useTransition ist der Companion-Hook zu React Suspense für Navigationsübergänge. Ohne useTransition zeigt React bei einer Navigation mit ausstehenden Daten sofort den Suspense-Fallback an — die aktuelle Seite verschwindet, ein Spinner erscheint, dann die neue Seite. Das ist funktional, aber visuell unbefriedigend. Mit useTransition bleibt die aktuelle Seite sichtbar, während die neuen Daten im Hintergrund geladen werden — und React zeigt nur dann den Fallback, wenn das Laden zu lang dauert.
Der Trick liegt in der Prioritätsvergabe: startTransition markiert einen State-Update als nicht-dringend. React kann niedrig-priorisierte Renders unterbrechen und die aktuelle UI bis zur Fertigstellung zeigen. Das isPending-Flag von useTransition ermöglicht es, während des Übergangs ein subtiles Lade-Indikator-Element (z. B. eine Progress-Bar) anzuzeigen, ohne den gesamten Inhalt durch einen Spinner zu ersetzen.
// useTransition: navigate without replacing current page with a spinner
import { useTransition, Suspense } from 'react';
function Navigation() {
const [isPending, startTransition] = useTransition();
const [page, setPage] = React.useState('home');
function navigate(nextPage) {
// Low-priority update: current page stays visible during load
startTransition(() => {
setPage(nextPage);
});
}
return (
<>
{/* Subtle loading indicator — no full-page spinner */}
{isPending && (
<div className="fixed top-0 inset-x-0 h-1 bg-sky-500 animate-pulse" />
)}
<nav>
<button onClick={() => navigate('products')} disabled={isPending}>
Products
</button>
</nav>
<Suspense fallback={<PageSkeleton />}>
<PageContent page={page} />
</Suspense>
</>
);
}
// useDeferredValue: show stale content while new data loads
function SearchResults({ query }) {
const deferredQuery = React.useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div style={ { opacity: isStale ? 0.5 : 1 } }>
<Suspense fallback={<ResultsSkeleton />}>
<Results query={deferredQuery} />
</Suspense>
</div>
);
}
7. Error Boundaries gemeinsam mit Suspense
Suspense behandelt den Lade-Zustand, aber nicht den Fehler-Zustand. Wenn ein von Suspense gewrappter Datenfetch fehlschlägt, wirft React den Fehler zur nächsten Error Boundary. Das bedeutet: Jede Suspense-Boundary sollte von einer Error Boundary umschlossen sein, die den Fehlerfall behandelt. In modernen React-Projekten übernimmt das oft eine kombinierte Komponente, die beide Boundaries zusammenfasst.
React bietet noch keine funktionale Error-Boundary-API — sie muss als Klassen-Komponente oder über eine Bibliothek wie react-error-boundary implementiert werden. In React 19 gibt es Diskussionen über eine funktionale API, aber noch kein stabiles Feature. Die react-error-boundary-Bibliothek liefert ErrorBoundary-Komponente mit fallbackRender-Prop und useErrorBoundary-Hook, was die Integration mit Suspense erheblich vereinfacht.
8. Suspense und Server Components
Server Components in Next.js App Router und React 19 sind die natürlichste Anwendung von Suspense für Datenfetching. Ein Server Component kann direkt await auf Daten verwenden — React wandelt das automatisch in eine Suspense-Boundary um. Das bedeutet: Der Datenfetch geschieht auf dem Server, der Client bekommt den bereits gerenderten HTML-Inhalt, und der Lade-Zustand ist eine Streaming-Boundary, die der Browser progressiv ausfüllt.
Das Streaming-Rendering von Next.js App Router basiert vollständig auf Suspense: Die Shell der Seite (Layout, Header, Navigation) wird sofort gesendet, Suspense-Boundaries werden als Platzhalter übermittelt und durch den gerenderten Inhalt ersetzt, sobald die Server-Daten bereit sind. Für den Nutzer sieht das aus wie progressives Laden ohne JavaScript-Bundle-Abhängigkeit. Für das Team bedeutet es: Server Components sind Suspense-nativ, kein zusätzliches Datenfetch-Framework nötig.
9. Suspense-Patterns im Vergleich
Die Entscheidung, welches Suspense-Pattern für welchen Use Case richtig ist, hängt von mehreren Faktoren ab: ob die Daten auf dem Server oder Client geladen werden, ob ein Übergang sofort oder verzögert angezeigt werden soll, und wie fein-granular die Lade-Zustände sein sollen.
| Pattern | Wann einsetzen | Vorteil | Einschränkung |
|---|---|---|---|
| React.lazy + Suspense | Code-Splitting für selten genutzte Features | Kleines initiales Bundle | Nur Client-Side |
| useSuspenseQuery | Client-seitiger Datenfetch mit React Query | Kein isLoading-Boolean, kein Race Condition | Bibliotheks-Abhängigkeit |
| use() + Promise | React 19, bibliotheksfreier Fetch | Kein externes Package nötig | Kein Caching ohne Wrapper |
| Server Components + await | Next.js App Router, Streaming SSR | Zero client-side JS für Datenfetch | Nur in Server-Umgebungen |
| useTransition | Navigationsübergänge ohne Flicker | Aktuelle Seite bleibt sichtbar | Zusätzliche Pending-UI nötig |
10. Zusammenfassung
React Suspense ist in React 18 und 19 zu einem vollständigen Lade-State-Management-System geworden. Die Grundidee — Komponenten zeigen durch ein geworfenes Promise an, dass sie warten — führt zu deklarativen Lade-Zuständen, die aus den Komponenten selbst herausgezogen werden. Die Suspense Boundary-Hierarchie kontrolliert, welche Teile der UI beim Laden sichtbar bleiben, und ermöglicht progressive Lade-Erfahrungen ohne isLoading-Booleans.
Die wichtigsten Prinzipien: Boundaries an sinnvollen UI-Einheiten platzieren, nicht eine pro Component. Fetches auf Route-Ebene starten, um Waterfalls zu vermeiden. useTransition für Navigationsübergänge nutzen, um Spinner-Flicker zu eliminieren. Server Components mit await für zero-JS-Datenfetching. Und immer eine Error Boundary um jede Suspense Boundary — der Fehler-Zustand ist genauso wichtig wie der Lade-Zustand.
React Suspense — Das Wichtigste auf einen Blick
Boundary-Positionierung
Suspense Boundaries an sinnvollen UI-Einheiten setzen — nicht eine pro Component. Feingranulare Boundaries ermöglichen progressives Laden ohne Layout-Shift.
Waterfall vermeiden
Fetches auf Route-Ebene starten, nicht tief in Komponenten. Promise.all oder prefetchQuery nutzen, um parallele Anfragen zu koordinieren.
useTransition
Navigationsübergänge ohne Spinner-Flicker — aktuelle Seite bleibt sichtbar, isPending für subtile Lade-Indikatoren nutzen.
Error Boundaries
Jede Suspense Boundary braucht eine umschließende Error Boundary. react-error-boundary vereinfacht die Integration mit fallbackRender und Reset-Logik.
Mironsoft
React-Performance, Suspense-Architektur und Next.js App Router
React-Anwendung mit Suspense optimieren?
Wir analysieren bestehende React-Codebases auf Render-Waterfalls, falsch platzierte Boundaries und isLoading-Anti-Patterns — und ersetzen sie durch eine saubere Suspense-Architektur.
Performance-Audit
Render-Waterfalls und Boundary-Platzierung analysieren und optimieren
Migration
isLoading-Pattern durch Suspense und useSuspenseQuery ersetzen
Next.js App Router
Server Components mit Streaming und Suspense-basiertem Datenfetch implementieren