Kurzfassung: Streaming SSR verwandelt das serverseitige Rendering von einem blockierenden „Alles-oder-Nichts"-Prozess in eine progressive Pipeline, die HTML an den Browser sendet, sobald es verfügbar ist. Durch die Nutzung von React 19s
renderToReadableStreamund strategisch platzierten Suspense-Grenzen lässt sich die Largest Contentful Paint (LCP) um 30-50 % verbessern, ohne SEO zu opfern. Dieser Leitfaden erklärt die internen Mechanismen, die Trade-offs und die Implementierung von Streaming mit TanStack Start.
Einleitung: Von SSR zu Streaming SSR
Server-side Rendering (SSR) galt lange als Goldstandard für schnelle initiale Seitenladezeiten und SEO-freundliche Inhalte. Doch herkömmliches SSR hat einen grundlegenden Nachteil: Es ist synchron. Der Server muss sämtliche Daten abrufen, den gesamten Komponentenbaum rendern und erst dann die fertige HTML-Antwort senden. Der Browser wartet untätig.
Streaming SSR kehrt dieses Paradigma um. Anstatt auf das vollständig gerenderte Seitenlayout zu warten, sendet der Server HTML-Chunks progressiv. Der Browser kann bereits mit dem Parsen beginnen, Ressourcen laden und sogar Teilinhalte darstellen, lange bevor die vollständige Antwort eingetroffen ist.
Funktionsweise von herkömmlichen SSR (Rendern → Warten → Senden)
Der konventionelle SSR-Ablauf:
1. Server empfängt Anfrage
2. Server ruft ALLE benötigten Daten ab (asynchrone Wasserfälle)
3. Server rendert gesamten React-Baum zu HTML-String
4. Server sendet vollständige HTML-Antwort
5. Browser empfängt HTML und beginnt mit dem Parsen
Das kritische Problem: Jede Datendependenz muss aufgelöst sein, bevor auch nur ein einziges Byte HTML den Browser erreicht. Wenn ein Footer Daten von einer langsamen API benötigt, werden Header und Hero-Section als Geiseln gehalten.
Hier eine vereinfachte Version des herkömmlichen blockierenden SSR-Musters:
// Traditionelles SSR – alles blockiert
async function renderPage(url) {
const data = await fetchAllData(url); // blockiert
const html = ReactDOMServer.renderToString(<App data={data} />); // blockiert
return sendResponse(html); // endlich wird gesendet
}
Dies erzeugt ein Head-of-Line-Blocking. TTFB leidet darunter, weil der Server nicht antworten kann, bis alles bereit ist.
Funktionsweise von Streaming SSR (Rendern → Streamen → Progressiv Verbessern)
Streaming SSR kehrt das Modell um:
1. Server empfängt Anfrage
2. Server beginnt sofort mit dem Rendern
3. Server sendet Shell-HTML (Header, Nav, Skeleton)
4. Sobald Daten aufgelöst sind, streamt der Server gerenderte Komponenten
5. Browser zeichnet Inhalte progressiv auf
6. Hydration findet statt, sobald der vollständige Stream abgeschlossen ist
Der Server braucht nicht alle Daten, um mit dem Senden von HTML zu beginnen. Das Seitengrundgerüst kann sofort gesendet werden. Sobald jede Suspense-Grenze ihre Daten auflöst, wird das gerenderte HTML gestreamt.
async function renderPage(url, res) {
const stream = await ReactDOMServer.renderToReadableStream(<App />, {
onError(error) {
console.error('Streaming error:', error);
}
});
await stream.pipeTo(new WritableStream({
write(chunk) { res.write(chunk); }
}));
res.end();
}
Der Browser empfängt HTML nahezu sofort, während der Server noch tieferliegende Komponenten rendert.
Hier das Streaming-Äquivalent mit React 19s renderToReadableStream:
Streaming-Primitiven in React 19
renderToReadableStream
Das Arbeitstier des Streaming SSR ist renderToReadableStream. So wird es in einem Server-Request-Handler verwendet:
import { renderToReadableStream } from 'react-dom/server';
async function handleRequest(req, res) {
const stream = await renderToReadableStream(
<HtmlShell><App /></HtmlShell>,
{
bootstrapScripts: ['/client.js'],
onError(err) {
console.error(err);
},
}
);
await stream.pipeTo(new WritableStream({
write(chunk) { res.write(chunk); }
}));
res.end();
}
Der Stream wird direkt an die Antwort weitergeleitet, sobald Chunks verfügbar sind. Anders als renderToString, das auf den gesamten Baum wartet, sendet dieser Ansatz HTML progressiv.
Die Rolle von Suspense-Grenzen
Suspense-Grenzen sind der Schlüssel, der Streaming ermöglicht. Jede <Suspense>-Grenze fungiert als unabhängige Rendereinheit. Dieses Layout zeigt, wie Inhalte über und unter dem Fold unterschiedlich behandelt werden können:
function Page() {
return (
<Layout>
<Header /> {/* sofort gesendet */}
<Suspense fallback={<Spinner />}>
<ArticleContent /> {/* gestreamt, sobald Daten bereit */}
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar /> {/* unabhängig gestreamt */}
</Suspense>
<Footer /> {/* sofort gesendet */}
</Layout>
);
}
Jede Suspense-Grenze hat ihren eigenen Zeitplan. Der Browser ersetzt progressiv Fallback-Platzhalter durch echte Inhalte.
Streaming-Implementierung mit TanStack Start
TanStack Start integriert Streaming SSR als erstklassiges Feature mit einem einfachen Konfigurationsflag. Hier die Server-Einrichtung, die Streaming aktiviert:
// app/server.ts
import { createStartHandler } from '@tanstack/start/server';
import { getRouterManifest } from '@tanstack/start/router-manifest';
import { createRouter } from './router';
export default createStartHandler({
createRouter,
getRouterManifest,
})({
streaming: true, // standardmäßig aktiviert
suspense: {
shellTimeout: 5_000,
emitLoadingStates: true,
},
});
Routing-Level-Datenladen mit Streaming
Das defer-Utility wird für nicht-kritische Daten verwendet, die nach dem initialen Shell-Rendering gestreamt werden können. Dieses Beispiel zeigt eine Blogpost-Seite, bei der der Artikelinhalt sofort lädt, während Empfehlungen und Kommentare gestreamt werden:
export const Route = createFileRoute('/blog/$slug')({
loader: async ({ params, context }) => {
return {
article: await context.db.getArticle(params.slug),
recommendations: defer(
context.db.getRelatedPosts(params.slug, { limit: 3 })
),
comments: defer(
context.db.getComments(params.slug),
{ timeout: 10_000 }
),
};
},
component: BlogPost,
});
function BlogPost() {
const { article, recommendations, comments } = Route.useLoaderData();
return (
<article>
<h1>{article.title}</h1>
<BlogContent content={article.content} />
<Suspense fallback={<RelatedSkeleton />}>
<Await promise={recommendations}>
{(recs) => <RelatedPosts posts={recs} />}
</Await>
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Await promise={comments}>
{(cmts) => <CommentsSection comments={cmts} />}
</Await>
</Suspense>
</article>
);
}
); }
Das `defer`-Utility weist den Router an: „Rendere jetzt alles, was möglich ist, und streame diese Daten, wenn sie bereit sind." Die `<Await>`-Komponente ist ein Suspense-bewusster Wrapper, der den Fallback rendert, bis das deferred Promise aufgelöst ist.
## Suspense-Strategien
### Regel 1: Die Shell-Grenze
Alle Inhalte unter dem Fold in mindestens eine Suspense-Grenze einwickeln, damit das Seitengrundgerüst sofort gerendert wird:
```jsx
function Document() {
return (
<html>
<head>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<AppShell />
<Suspense fallback={null}>
<PageContent />
</Suspense>
</body>
</html>
);
}
Regel 2: Unabhängige Daten = Unabhängige Grenze
Wenn zwei Komponenten von unterschiedlichen Endpunkten abrufen, verwenden Sie separate Suspense-Grenzen, damit eine langsame API nicht die andere blockiert:
{/* SCHLECHT – eine langsame API blockiert beide */}
<Suspense fallback={<Spinner />}>
<UserProfile />
<ActivityFeed />
</Suspense>
{/* GUT – unabhängiges Streaming */}
<Suspense fallback={<ProfileSkeleton />}><UserProfile /></Suspense>
<Suspense fallback={<FeedSkeleton />}><ActivityFeed /></Suspense>
Regel 3: Priorität für Above-the-Fold-Inhalte
Above-the-Fold-Inhalte sollten außerhalb von Suspense oder in schnell auflösenden Grenzen platziert werden.
Leistungsauswirkungen: TTFB vs. LCP
| Metrik | Traditionelles SSR | Streaming SSR | Auswirkung |
|---|---|---|---|
| First Byte | ~200-800ms | ~50-100ms | Schneller |
| Shell Paint | ~400-1200ms | ~100-300ms | Schneller |
| LCP | ~1500-4000ms | ~800-2500ms | 30-50 % besser |
| Time to Interactive | ~2000-5000ms | ~1500-3500ms | Schneller |
LCP verbessert sich, weil der Browser kritische Ressourcen lädt, während der Server noch tieferliegende Seitenbereiche rendert.
Fehlerbehandlung beim Streaming
Error Boundaries beim Streaming
Error Boundaries umschließen Suspense-Grenzen, um Fehler elegant zu behandeln, ohne die gesamte Seite zum Absturz zu bringen:
function SafeSection({ children }) {
return (
<ErrorBoundary fallback={<p>Dieser Abschnitt konnte nicht geladen werden.</p>}>
<Suspense fallback={<SectionSkeleton />}>
{children}
</Suspense>
</ErrorBoundary>
);
}
Shell Timeout
Konfigurieren Sie ein Shell-Timeout in TanStack Start, damit der Server auf eine statische Version zurückfällt, wenn das Laden zu lange dauert:
createStartHandler({...})({
streaming: true,
suspense: {
shellTimeout: 5_000,
onShellTimeout() {
return handleStaticFallback();
},
},
});
SEO-Überlegungen
Ein hartnäckiger Mythos besagt, dass Streaming SSR die SEO beeinträchtigt. Das ist falsch. Googlebot unterstützt Streaming HTML seit 2020. Googles John Mueller hat ausdrücklich bestätigt, dass Googlebot gestreamtes HTML inkrementell verarbeitet.
Stellen Sie dennoch sicher, dass kritische SEO-Inhalte (Title-Tags, Meta-Descriptions, H1) in der Shell oder früh gestreamten Chunks enthalten sind. So sehen Suchmaschinen die wesentlichen Metadaten sofort:
function Document({ url, meta }) {
return (
<html>
<head>
<title>{meta.title}</title>
<meta name="description" content={meta.description} />
<link rel="canonical" href={meta.canonical} />
</head>
<body>
<Suspense fallback={<Loading />}>
<MainContent />
</Suspense>
</body>
</html>
);
}
Wann Streaming nicht hilft
- Kleine Seiten ohne Datenabrufe – traditionelles SSR ist schneller
- Seiten, auf denen alle Daten in unter 100 ms aufgelöst werden – kaum messbarer Vorteil
- Stark缓存fähige Seiten, die vom CDN Edge ausgeliefert werden
- API-Antworten, die bereits schnell sind – Datenbankabfragen unter 50 ms
Streaming entfaltet seine größte Wirkung bei datenreichen, personalisierten oder dynamischen Seiten, bei denen ein Teil der Inhalte schnell und ein anderer langsam ist.
Fazit
Streaming SSR stellt einen grundlegenden Paradigmenwechsel dar. Anstatt die HTML-Antwort als monolithischen Block zu behandeln, wird sie in unabhängige Chunks aufgeteilt, die inkrementell erzeugt und verarbeitet werden können.
Wichtigste Erkenntnisse:
- Streaming SSR sendet HTML progressiv, nicht als einen Block
- React 19s
renderToReadableStreamund Suspense-Grenzen machen dies praktikabel - TanStack Start bietet erstklassiges Streaming mit der
defer-API - LCP verbessert sich um 30-50 % bei datenreichen Seiten
- SEO wird nicht negativ beeinträchtigt
- Messen Sie vor der Einführung; Streaming ist keine Allzwecklösung
Weitere Informationen finden Sie im Lighthouse 95+ Optimization Playbook zur Messung der Leistungsauswirkungen von Streaming. Erfahren Sie, wie Sie Streaming SSR mit Full-Stack SSR on Cloudflare Workers bereitstellen, und der TanStack Start Deployment Guide führt durch die Produktions-Deploying-Konfiguration.