Suspense zeigt einen Fallback, etwa eine Lademeldung, solange die Komponenten darin noch nicht bereit sind. React.lazy ist das, worauf am häufigsten gewartet wird: Es lädt den Code einer Komponente erst, wenn diese Komponente zum ersten Mal rendert, was den ersten Download klein hält.
Klicke auf Show chart: Loading chart... erscheint für eine Sekunde, dann das Diagramm. Versteck es und zeig es erneut, und es erscheint sofort ohne neue Log-Zeile, weil lazy das geladene Modul behält.
Code Splitting mit lazy
Der Editor hält alles in einer Datei, also baut das Beispiel das langsame Modul von Hand: ein Promise, das nach einer Sekunde zu einem Objekt mit einem Export default aufgelöst wird. In einer echten App lebt die Komponente in einer eigenen Datei, und du übergibst einen dynamischen Import:
import { lazy, Suspense } from 'react';
const Chart = lazy(() => import('./Chart.jsx'));
export default function Dashboard() {
return (
<Suspense fallback={<p>Loading chart...</p>}>
<Chart />
</Suspense>
);
}
import('./Chart.jsx') gibt ein Promise für das Modul zurück. Bundler wie Vite und webpack erkennen den dynamischen Import und legen Chart.jsx und alles, was nur sie nutzt, in eine separate Datei, die heruntergeladen wird, wenn <Chart /> zum ersten Mal rendert. Regeln, die du kennen solltest:
- Das Modul braucht einen Default-Export.
lazyliest die Eigenschaftdefaultdessen, womit das Promise aufgelöst wird. Für einen benannten Export bildest du ihn ab:lazy(() => import('./charts.js').then((m) => ({ default: m.LineChart }))). - Ruf
lazyauf der obersten Ebene eines Moduls auf. In einer Komponente würde es bei jedem Render einen neuen Komponententyp erzeugen, also würde React die alte unmounten, ihren State verlieren und sie neu laden. - Teile dort, wo ohnehin gewartet wird. Routen, Modals, selten geöffnete Panels und schwere Widgets (Editoren, Diagramme, Karten) sind gute Kandidaten. Jede kleine Komponente aufzuteilen bringt zusätzliche Anfragen und Ladezustände ohne Gewinn.
Vor dem Klick vorladen
Eine lazy Komponente beginnt zu laden, wenn sie zum ersten Mal rendert, also wartet man nach dem Klick immer mindestens einen Download. Wenn du ahnen kannst, dass der Klick kommt, starte den Download früher. Halte die Import-Funktion in einer Variablen und ruf sie bei Hover oder Fokus auf; der Browser behält das Modul, also wird derselbe Import, wenn lazy ihn später aufruft, ohne zweiten Download aufgelöst.
const loadChart = () => import('./Chart.jsx');
const Chart = lazy(loadChart);
<button onMouseEnter={loadChart} onFocus={loadChart} onClick={() => setShow(true)}>
Show chart
</button>
Wie Suspense entscheidet, was es zeigt
Wenn eine Komponente in <Suspense> nicht bereit ist, suspendiert sie: React hört auf, diesen Teil zu rendern, und zeigt den fallback des nächsten Suspense darüber. Alles in dieser Grenze wird durch den Fallback ersetzt, nicht nur die wartende Komponente. Wenn das, worauf sie gewartet hat, bereit ist, rendert React den Inhalt erneut und ersetzt den Fallback.
Damit ist die Position der Grenze eine Designentscheidung. Setze unabhängige Teile in eigene Grenzen, damit jeder erscheinen kann, wenn er bereit ist:
Zuerst zeigt die ganze Seite Loading page..., weil Header zur äußeren Grenze gehört. Sobald der Header bereit ist, erscheint der Artikel mit Loading comments... darunter, und die Kommentare kommen zuletzt. Ändere 1500 zu 3000, und die Vorschau lädt neu, mit längerer Wartezeit nur für die Kommentare. Lösch das innere <Suspense> (<Comments /> bleibt), und die Seite wartet auf die Kommentare, bevor sie irgendetwas zeigt.
Suspense mit use() in React 19
In React 19 kann eine Komponente ein Promise mit use(promise) lesen. Wenn das Promise noch aussteht, suspendiert die Komponente, und das nächste Suspense zeigt seinen Fallback; wenn es aufgelöst ist, gibt use den Wert zurück. Die Seite zum use-Hook behandelt das vollständig. Das Fake-fetchUser unten steht für eine echte Anfrage.
Klicke dich durch die Nutzer und geh dann zurück zu User 1: Er erscheint sofort, und die Konsole loggt keinen neuen Fetch, weil das Promise schon im Cache liegt.
Warum das Promise gecacht sein muss
Die Map cache ist hier keine Optimierung, sondern Pflicht. Eine Komponente, die suspendiert, behält nichts aus diesem Versuch: Wenn das Promise aufgelöst ist, rendert React sie von oben neu. Würde Profile fetchUser(id) direkt aufrufen, würde jeder Versuch ein neues Promise erzeugen, eine neue Anfrage starten und wieder darauf suspendieren. Das Profil erscheint nie, und die Konsole füllt sich mit Zeilen fetching user. Das Promise muss also von einer Stelle kommen, die den Render überdauert:
- aus einem Cache, geordnet nach der Anfrage, wie die Map oben (Datenbibliotheken wie TanStack Query und Framework-Loader erledigen das für dich);
- von einem Elternteil, das das Promise einmal erzeugt, in einem Event-Handler oder einer Server Component, und es als Prop nach unten reicht.
Beachte, dass jeder neue Nutzer das Profil weiterhin durch den Fallback ersetzt. Wenn du lieber den alten Nutzer auf dem Bildschirm behältst, bis der nächste bereit ist, pack das Update in eine Transition: startTransition(() => setId(n)). Bei einer Transition versteckt React keinen Inhalt, der schon sichtbar ist (siehe useTransition).
Fehler brauchen eine Error Boundary
Suspense kümmert sich ums Warten, nicht ums Scheitern. Wenn ein lazy Import fehlschlägt (jemand ist offline gegangen, ein neues Deployment hat den alten Chunk entfernt) oder ein an use übergebenes Promise abgelehnt wird, wirft React den Fehler an die nächste Error Boundary. Ohne eine wird der ganze Baum unter dem Root unmountet. Error Boundaries sind weiterhin Klassenkomponenten (siehe Error Boundaries):
<ErrorBoundary fallback={<p>Could not load the chart.</p>}>
<Suspense fallback={<p>Loading chart...</p>}>
<Chart />
</Suspense>
</ErrorBoundary>
Was Suspense nicht erkennt
Suspense reagiert nur auf Komponenten, die suspendieren: lazy-Komponenten, use(promise) und für Suspense gebaute Datenquellen (Framework-Loader, Bibliotheken mit Suspense-Unterstützung). Ein Fetch in useEffect, der nach dem Abschluss State setzt, suspendiert nicht, also zeigt eine Suspense-Grenze darum nie ihren Fallback. Für dieses Muster hältst du deinen eigenen loading-State, wie auf der Seite zum Laden von Daten gezeigt.
Häufig gestellte Fragen
Was ist React Suspense?
<Suspense fallback={...}> ist eine Komponente, die ihren fallback zeigt, solange eine Komponente darin auf etwas wartet, etwa lazy geladenen Code oder mit use gelesene Daten. Wenn alles darin bereit ist, tauscht React den Fallback gegen den Inhalt.
Was macht React.lazy?
lazy(() => import('./Chart.jsx')) erzeugt eine Komponente, deren Code beim ersten Rendern heruntergeladen wird. Bundler legen diese Datei in einen separaten Chunk, also lädt die erste Seite weniger JavaScript.
Funktioniert Suspense beim Laden von Daten?
Ja, wenn die Datenquelle es unterstützt. In React 19 kann eine Komponente ein Promise mit use(promise) lesen und suspendieren, bis es aufgelöst ist. Frameworks wie Next.js binden Suspense außerdem in ihr Laden von Daten ein. Ein Fetch in useEffect löst Suspense nicht aus.
Wie behandle ich Fehler mit Suspense?
Suspense kümmert sich nur ums Warten. Wenn ein lazy Import oder ein Promise fehlschlägt, geht der Fehler an die nächste Error Boundary, also umschließe die Suspense-Grenze (oder ihr Elternteil) mit einer.
Wo sollte ich lazy aufrufen?
Auf der obersten Ebene eines Moduls, außerhalb jeder Komponente. lazy in einer Komponente aufzurufen erzeugt bei jedem Render einen neuen Komponententyp, was ihren State zurücksetzt und sie neu lädt.