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.
Inhaltsverzeichnis
- 1. Warum klassisches SSR mit einer Wartezeit für die komplette Seite bezahlt wird
- 2. Wie renderToPipeableStream HTML in Chunks an den Browser sendet
- 3. Time-to-First-Byte-Verbesserung durch frühes Senden des Shells
- 4. Suspense-Boundaries für gezieltes Streaming langsamer Seitenbereiche
- 5. Selective Hydration: Interaktivität einzelner Bereiche vor dem vollständigen Laden
- 6. onShellReady und onAllReady: Wann welcher Callback greift
- 7. Fehlerbehandlung beim Streaming: onShellError, onError und Bot-Erkennung
- 8. Streaming SSR und CDN-Caching: was sich ändert
- 9. Die tatsächliche Verbesserung im Real User Monitoring messen
- 10. Zusammenfassung
- 11. FAQ
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.