Streaming SSR mit React 18: Die konkreten Performance-Vorteile im Detail
AI generated
60fps
ms
Performance · React 18 · Streaming SSR
Streaming SSR mit React 18
Wie serverseitiges Rendering in Chunks messbare Performance-Vorteile bringt

Klassisches serverseitiges Rendering zwingt den Server dazu, den gesamten Komponentenbaum einer Seite vollständig durchzurendern, bevor auch nur das erste Byte an den Browser gesendet werden darf. Hängt irgendeine Komponente auf einer langsamen Datenbankabfrage oder einem externen API-Call, wartet der komplette Response so lange, wie die langsamste Komponente braucht, selbst wenn neun von zehn Bereichen der Seite längst fertig wären. React 18 löst dieses Problem mit echtem Streaming SSR: renderToPipeableStream sendet das HTML in mehreren Chunks, sobald sie fertig sind, statt auf die komplette Seite zu warten. Kombiniert mit Suspense-Boundaries lässt sich gezielt steuern, welche Bereiche sofort und welche verzögert nachgeliefert werden, mit messbaren Verbesserungen bei Time-to-First-Byte und wahrgenommener Ladezeit.

15 Min. Lesezeit React 18 · Streaming SSR Suspense · Selective Hydration

1. Warum klassisches SSR mit einer Wartezeit für die komplette Seite bezahlt wird

Bei einem klassischen, blockierenden SSR-Aufruf durchläuft der Server den kompletten React-Baum von oben nach unten und wartet bei jeder Komponente, die Daten nachlädt, synchron auf das Ergebnis, bevor die Ausgabe fortgesetzt werden kann. Erst wenn wirklich jede einzelne Komponente fertig gerendert ist, entsteht der vollständige HTML-String, der dann in einem einzigen Response an den Browser geschickt wird. Für eine Seite mit einer schnellen Kopfzeile, aber einem langsamen, personalisierten Empfehlungsbereich weiter unten bedeutet das, dass der Nutzer buchstäblich auf die langsamste Komponente wartet, obwohl der große Rest der Seite technisch längst bereitstehen würde.

Diese Kopplung zwischen der langsamsten und der schnellsten Komponente einer Seite ist der eigentliche Kern des Problems, denn sie macht die gesamte Ladezeit von einem einzigen, oft nicht kritischen Seitenbereich abhängig. Eine Produktdetailseite mit schnellem Haupt-Content, aber einer langsamen Bewertungs-API im unteren Bereich liefert im klassischen SSR-Modell in der Summe genauso langsam aus wie die Bewertungs-API selbst braucht, obwohl der Käufer die Bewertungen oft erst nach dem Scrollen überhaupt sieht. Genau hier setzt Streaming SSR an, indem es die Abhängigkeit zwischen Seitenbereichen und Ladezeit auftrennt.

2. Wie renderToPipeableStream HTML in Chunks an den Browser sendet

renderToPipeableStream ersetzt die ältere renderToString-Funktion für Node-Server und gibt statt eines fertigen Strings einen Node-Stream zurück, den der Server chunkweise an den Response weiterreichen kann, sobald neue Teile des HTML feststehen. Intern arbeitet React dabei mit einem sogenannten Shell, dem statischen Grundgerüst der Seite ohne die auf Daten wartenden Bereiche, das als erster Chunk gesendet wird, sobald es fertig ist. Alle Bereiche, die in eine Suspense-Boundary eingebettet sind und noch auf Daten warten, werden zunächst durch ihren Fallback-Zustand ersetzt und erst nachträglich als weiterer Chunk inklusive eines kleinen Inline-Scripts nachgeliefert, das den Fallback im Browser gegen den finalen Inhalt austauscht.

Der entscheidende Unterschied zu renderToString ist also nicht nur eine technische Umstellung der API, sondern eine grundlegend andere Zeitachse: Statt eines einzigen Zeitpunkts, an dem alles fertig ist, gibt es jetzt mehrere Zeitpunkte, an denen jeweils ein weiterer Teil der Seite feststeht und ausgeliefert wird. Das folgende Beispiel zeigt eine minimale Express-Route, die renderToPipeableStream nutzt und den Stream direkt in die Response pipet, sobald der Shell bereit ist.


// Express-Route mit renderToPipeableStream
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';

app.get('/produkt/:id', (req, res) => {
  const { pipe, abort } = renderToPipeableStream(<App url={req.url} />, {
    bootstrapScripts: ['/client.js'],
    onShellReady() {
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/html');
      pipe(res); // Shell wird sofort gesendet, weitere Chunks folgen
    },
    onShellError(error) {
      res.statusCode = 500;
      res.send('<h1>Serverfehler</h1>');
    },
    onError(error) {
      console.error('Streaming-Fehler nach dem Shell:', error);
    },
  });

  setTimeout(abort, 10000); // Sicherheitsnetz gegen haengende Requests
});

3. Time-to-First-Byte-Verbesserung durch frühes Senden des Shells

Time to First Byte misst, wie lange der Browser wartet, bis das erste Byte der Response eintrifft, und ist bei klassischem SSR direkt an die langsamste Datenabfrage der gesamten Seite gekoppelt. Mit Streaming SSR verschiebt sich diese Messgröße fundamental, denn der Shell, also der statische Grundaufbau ohne die datenabhängigen Bereiche, kann gesendet werden, sobald er fertig gerendert ist, unabhängig davon, wie lange einzelne Suspense-umschlossene Bereiche noch brauchen. Für eine typische Produktseite bedeutet das oft eine TTFB-Verbesserung im Bereich von mehreren hundert Millisekunden bis zu einer Sekunde, je nachdem wie langsam die ausgelagerten Datenquellen im Vergleich zum Rest der Seite sind.

Wichtig ist dabei zu verstehen, dass sich nicht die absolute Serverarbeitszeit verkürzt, sondern der Zeitpunkt, an dem der Browser mit dem Rendern beginnen kann, deutlich früher liegt. Der Browser kann bereits Styles anwenden, das Layout aufbauen und mit dem Parsen weiterer Ressourcen beginnen, während der Server im Hintergrund noch an den verzögerten Bereichen arbeitet. Diese Verschiebung wirkt sich direkt auf nutzerwahrgenommene Metriken wie First Contentful Paint aus, da der Browser frühe, sichtbare Inhalte darstellen kann, ohne auf die komplette Serverantwort warten zu müssen.

4. Suspense-Boundaries für gezieltes Streaming langsamer Seitenbereiche

Eine Suspense-Boundary markiert im Komponentenbaum explizit einen Bereich, dessen Inhalt asynchron nachgeladen werden darf, ohne dass die umgebende Seite darauf warten muss. Beim Streaming SSR entscheidet die Platzierung dieser Boundaries direkt darüber, welche Seitenbereiche zum Shell gehören und damit sofort gesendet werden, und welche als separate, später folgende Chunks ausgeliefert werden. Eine sinnvolle Strategie ist, kritische, für die Kaufentscheidung relevante Bereiche wie Produktbild, Preis und Warenkorb-Button außerhalb jeder Suspense-Boundary zu halten, während nachrangige Bereiche wie Bewertungen, ähnliche Produkte oder personalisierte Empfehlungen bewusst in eigene Boundaries eingebettet werden.

Die Granularität der Boundaries hat direkten Einfluss auf die Nutzererfahrung, denn zu grobe Boundaries verzögern große Seitenbereiche gemeinsam, während zu feine Boundaries viele kleine, sichtbare Nachlade-Ruckler erzeugen können, wenn Fallbacks nacheinander gegen finalen Content getauscht werden. Eine bewährte Herangehensweise ist, Boundaries entlang tatsächlicher Datenabhängigkeiten zu ziehen, sodass ein Bereich, der von einer einzigen langsamen API abhängt, als zusammenhängende Einheit streamt, statt mehrere voneinander unabhängige Ladezustände gleichzeitig auf dem Bildschirm zu zeigen.

5. Selective Hydration: Interaktivität einzelner Bereiche vor dem vollständigen Laden

Selective Hydration ist die logische Ergänzung zum Streaming auf der Client-Seite: Statt zu warten, bis der komplette Baum heruntergeladen und hydriert ist, kann React bereits ausgelieferte Bereiche hydrieren, sobald deren JavaScript verfügbar ist, unabhängig davon, ob spätere Chunks noch unterwegs sind. Klickt ein Nutzer auf einen bereits hydrierten Bereich, etwa den Warenkorb-Button im Shell, priorisiert React dessen Hydration automatisch gegenüber noch nicht interagierten Bereichen, selbst wenn diese im Baum weiter oben stehen.

Dieses Verhalten unterscheidet sich fundamental vom klassischen Hydration-Modell, bei dem die gesamte Seite erst vollständig interaktiv wird, nachdem wirklich der letzte Teil an JavaScript ausgeführt wurde. Für große, komponentenreiche Seiten reduziert Selective Hydration die Zeit bis zur tatsächlichen Interaktivität spürbar, weil kritische Interaktionselemente nicht mehr hinter langsameren, nachrangigen Bereichen in der Hydration-Warteschlange stehen, sondern bevorzugt behandelt werden, sobald der Nutzer mit ihnen interagieren möchte.

6. onShellReady und onAllReady: Wann welcher Callback greift

renderToPipeableStream stellt mehrere Callbacks zur Verfügung, die jeweils an einem anderen Punkt im Streaming-Prozess ausgelöst werden und unterschiedliche Anwendungsfälle bedienen. onShellReady feuert, sobald der statische Shell fertig ist und gesendet werden kann, und ist für die allermeisten Anwendungen der richtige Zeitpunkt, um mit pipe(res) zu beginnen, da er die schnellstmögliche Auslieferung ermöglicht. onAllReady dagegen wartet, bis wirklich der komplette Baum inklusive aller Suspense-Bereiche fertig ist, was für Anwendungsfälle wie Crawler oder statische Exporte relevant ist, bei denen ein vollständiges, sofort konsistentes HTML-Dokument gebraucht wird.

Die Wahl zwischen diesen beiden Callbacks ist keine rein technische Entscheidung, sondern hängt direkt vom Empfänger der Antwort ab. Ein regulärer Browser-Nutzer profitiert von onShellReady, weil er so schnell wie möglich mit dem Rendern beginnen kann, während ein Suchmaschinen-Crawler, der kein JavaScript nachlädt, unter Umständen nur mit onAllReady ein für die Indexierung vollständiges Dokument erhält. Viele Implementierungen prüfen deshalb den User-Agent und schalten für bekannte Bots gezielt auf onAllReady um, während echte Nutzer weiterhin vom frühen Shell-Versand profitieren.

7. Fehlerbehandlung beim Streaming: onShellError, onError und Bot-Erkennung

Streaming SSR bringt eine neue Klasse von Fehlerszenarien mit sich, da ein Fehler nach dem Versand des Shells nicht mehr einfach zu einer alternativen Fehlerseite führen kann, weil der Browser bereits einen Teil der Antwort empfangen und mit dem Rendern begonnen hat. onShellError fängt Fehler ab, die noch vor dem Versand des Shells auftreten, und erlaubt in diesem Fall noch einen sauberen Fallback auf eine komplette Fehlerseite mit korrektem Statuscode. onError dagegen behandelt Fehler, die erst während des Nachladens späterer Suspense-Bereiche auftreten, nachdem der Shell bereits unterwegs ist, und kann in diesem Fall nur noch für Logging und eine gezielte Fallback-Anzeige innerhalb der betroffenen Boundary genutzt werden.

Ein weiterer wichtiger Aspekt ist der Umgang mit hängenden Requests, etwa wenn eine externe API nie antwortet: Ohne ein explizites Timeout würde der Server die Verbindung theoretisch unbegrenzt offen halten. Die abort-Funktion, die renderToPipeableStream zurückgibt, erlaubt es, nach einer definierten Zeitspanne alle noch offenen Suspense-Bereiche zwangsweise mit ihrem Fallback-Zustand abzuschließen, damit der Request in jedem Fall terminiert und der Nutzer nicht endlos auf einen Ladezustand blickt, der niemals aufgelöst wird.

8. Streaming SSR und CDN-Caching: was sich ändert

Ein gestreamter Response lässt sich nicht auf dieselbe Weise cachen wie eine klassische, vollständige HTML-Antwort, denn ein CDN müsste theoretisch den kompletten Chunk-Verlauf inklusive Timing zwischenspeichern, um ihn identisch erneut auszuliefern. In der Praxis lösen die meisten Setups dieses Problem, indem sie den Shell getrennt von den nachfolgenden, oft personalisierten Chunks betrachten: Der Shell, der für viele Nutzer identisch ist, kann weiterhin am Edge gecacht werden, während die datenabhängigen, später gestreamten Bereiche grundsätzlich uncachebar bleiben und bei jedem Request frisch generiert werden.

Diese Trennung erfordert eine bewusste Architekturentscheidung: Bereiche, die stark personalisiert sind, etwa ein eingeloggter Nutzername oder ein individueller Warenkorb-Inhalt, sollten konsequent in eigene Suspense-Boundaries ausgelagert werden, damit der cachebare Shell so groß wie möglich bleibt. Wird stattdessen Personalisierung tief in den Shell selbst eingebaut, verliert der gesamte Response seine Cachebarkeit, und der Performance-Vorteil des Edge-Cachings geht verloren, obwohl Streaming technisch weiterhin funktioniert.

9. Die tatsächliche Verbesserung im Real User Monitoring messen

Die tatsächliche Verbesserung durch Streaming SSR zeigt sich nicht in synthetischen Labor-Messungen allein, sondern vor allem im Real User Monitoring, da die Auswirkung stark von der Verteilung der Ladezeiten einzelner Datenquellen in der echten Nutzerbasis abhängt. Ein A/B-Test zwischen klassischem und gestreamtem SSR auf identischem Traffic liefert die verlässlichsten Zahlen, weil er Schwankungen durch Tageszeit, Gerätetyp oder Netzwerkqualität auf beide Varianten gleichmäßig verteilt, statt sie als Störfaktor in einem Vorher-Nachher-Vergleich zu vermischen.

Neben TTFB und First Contentful Paint lohnt sich ein gezielter Blick auf die Interaction to Next Paint bei den ersten Nutzerinteraktionen, da Selective Hydration hier oft die größte spürbare Verbesserung bringt. Seiten mit vielen unabhängigen, datengetriebenen Bereichen und einer klaren Trennung zwischen kritischem und nachrangigem Content profitieren erfahrungsgemäß am stärksten, während sehr einfache Seiten mit wenigen oder keinen langsamen Datenabhängigkeiten kaum einen messbaren Unterschied zeigen, weil es dort schlicht nichts zu streamen gibt, das nicht ohnehin schon schnell wäre.

Metrik Klassisches SSR Streaming SSR Ursache des Unterschieds
Time to First Byte Wartet auf die langsamste Komponente der gesamten Seite Shell wird gesendet, sobald er fertig ist Datenabhängige Bereiche blockieren den Versand nicht mehr
First Contentful Paint Erst nach vollständiger Serverantwort möglich Browser kann mit dem empfangenen Shell sofort rendern Frühere Verfügbarkeit von sichtbarem HTML
Time to Interactive Gesamte Seite wird gemeinsam hydriert Selective Hydration priorisiert bereits geladene, interagierte Bereiche Hydration folgt Nutzerinteraktion statt fester Baumreihenfolge
Cachebarkeit am CDN Gesamte Antwort cachebar oder gar nicht Shell separat cachebar, personalisierte Chunks nicht Trennung zwischen statischem Shell und dynamischen Bereichen

Mironsoft

Web Performance, Core Web Vitals und Ladezeit-Optimierung

Ladezeiten, die Nutzer nicht abspringen lassen, bevor die Seite überhaupt sichtbar ist?

Wir prüfen bestehende Webseiten auf langsame Core Web Vitals, aufgeblähte JavaScript-Bundles und ungenutzte Render-Blocker und bauen daraus eine Performance-Grundlage, die messbar bleibt statt nur einmalig gut auszusehen.

Performance-Audit

Core Web Vitals, Ladewasserfall und Render-Blocker systematisch messen und beheben.

Bundle-Optimierung

JavaScript- und CSS-Bundle-Größe sowie Code-Splitting gezielt reduzieren.

Monitoring-Aufbau

Kontinuierliches Performance-Monitoring statt einmaliger Momentaufnahme etablieren.

10. Zusammenfassung

Streaming SSR mit React 18: Das Wichtigste auf einen Blick

Kernidee

renderToPipeableStream sendet den Shell sofort und liefert datenabhängige Bereiche als separate Chunks nach, statt auf die komplette Seite zu warten.

Größter Hebel

Die Platzierung von Suspense-Boundaries entscheidet, welche Bereiche zum sofort gesendeten Shell gehören und welche verzögert folgen.

Client-seitige Ergänzung

Selective Hydration priorisiert bereits geladene, vom Nutzer angeklickte Bereiche gegenüber der ursprünglichen Baumreihenfolge.

Caching

Nur der Shell lässt sich zuverlässig am Edge cachen, personalisierte, später gestreamte Bereiche bleiben grundsätzlich dynamisch.

11. FAQ: Streaming SSR mit React 18: Das Wichtigste auf einen Blick

1Was ist der Hauptunterschied zwischen renderToString und renderToPipeableStream?
renderToString liefert erst einen fertigen HTML-String, wenn wirklich der gesamte Baum gerendert ist, während renderToPipeableStream das HTML in mehreren Chunks sendet, sobald einzelne Teile fertig sind.
2Muss ich meine gesamte Anwendung umbauen, um Streaming SSR zu nutzen?
Nein, Streaming SSR lässt sich schrittweise einführen, indem einzelne, datenabhängige Bereiche nach und nach in Suspense-Boundaries eingebettet werden, während der Rest der Anwendung unverändert bleibt.
3Wann sollte ich onShellReady statt onAllReady verwenden?
Für normale Browser-Nutzer ist onShellReady fast immer richtig, weil es die schnellstmögliche Auslieferung ermöglicht. onAllReady eignet sich für Bots oder Anwendungsfälle, die ein vollständiges Dokument in einer einzigen Antwort brauchen.
4Wie verhindere ich, dass eine langsame API den gesamten Server blockiert?
Über die abort-Funktion von renderToPipeableStream lässt sich ein Timeout definieren, nach dem noch offene Suspense-Bereiche zwangsweise mit ihrem Fallback abgeschlossen werden.
5Funktioniert Streaming SSR mit jedem CDN?
Grundsätzlich ja, allerdings sollte der cachebare Shell architektonisch von personalisierten, später gestreamten Bereichen getrennt werden, damit der Edge-Cache weiterhin greifen kann.
6Was passiert, wenn JavaScript im Browser deaktiviert ist?
Der Shell wird weiterhin als HTML angezeigt, aber Selective Hydration und das Nachladen der Suspense-Bereiche setzen aktiviertes JavaScript voraus, sodass nachrangige Bereiche ohne JavaScript in ihrem Fallback-Zustand verbleiben können.
7Beeinflusst Streaming SSR die SEO-Indexierung?
Für moderne Crawler, die JavaScript ausführen, meist nicht spürbar. Für Crawler ohne JavaScript-Ausführung empfiehlt sich, per User-Agent-Erkennung auf onAllReady umzuschalten, um ein vollständiges Dokument zu liefern.
8Wie granular sollten Suspense-Boundaries gesetzt werden?
Entlang tatsächlicher Datenabhängigkeiten: Ein Bereich, der von derselben langsamen API abhängt, sollte als eine zusammenhängende Boundary streamen, statt in viele kleine, unabhängig ladende Fragmente zerlegt zu werden.
9Verbessert Streaming SSR auch die Serverauslastung?
Nicht direkt die Rechenzeit, aber die Verbindungsdauer pro Request kann sich verändern, da der Server früher mit dem Senden beginnt und Chunks parallel zur weiteren Verarbeitung ausliefert.
10Ist Streaming SSR nur für React 18 verfügbar?
renderToPipeableStream ist eine React-18-Funktion, aber das zugrunde liegende Konzept des chunkweisen HTML-Versands existiert in ähnlicher Form auch in anderen Frameworks mit eigener Streaming-Implementierung.