Stell dir eine Dashboard-Seite vor: Ganz oben der Username (kommt aus dem Cookie, ist sofort da). Mittendrin eine teure Statistik (braucht 2 Sekunden Datenbankzeit). Ohne weitere Vorkehrungen müsste der Server warten, bis alle Daten da sind, bevor er auch nur ein Byte HTML zurückschicken kann. Der Nutzer sieht zwei Sekunden lang Nichts. Mit Streaming lieferst du die schnellen Inhalte sofort und das langsame Stück folgt, sobald es bereit ist. Dieser Artikel zeigt das Pattern.

Das Konzept hinter Streaming

Eine HTTP-Antwort muss nicht in einem Stück kommen. Der Server kann den Status-Code und die ersten Header sofort schicken, dann nach und nach mehr HTML, schließlich das Schluss-Tag. Der Browser zeigt schon, was er hat, während weitere Teile noch geladen werden.

Das ist nicht neu — Browser können das seit Jahrzehnten. SvelteKit nutzt es geschickt: Du kannst aus deiner load-Funktion ungewartete Promises zurückgeben. SvelteKit liefert das HTML mit Lade-Zustand sofort aus, schickt die Promise-Daten nach, sobald sie da sind, und der Browser füllt die Stelle automatisch mit dem Endergebnis.

Das einfachste Streaming-Beispiel

ts src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ locals }) => {
    // Schnell: User aus dem Cookie
    const user = locals.user;

    // Langsam: teure Statistik — als ungewartetes Promise zurückgeben
    const statsPromise = computeStats(locals.user.id);

    return {
        user,
        stats: statsPromise, // KEIN await!
    };
};

Beachte: stats ist ein Promise, kein aufgelöster Wert. Wenn du wartest (await statsPromise), würde der Load-Aufruf erst nach 2 Sekunden zurückkommen. Ohne await zurückgeben heißt: Sofort weiter, das Promise wird mitgeschickt.

In der Komponente nutzt du den {#await}-Block, den du aus dem Template-Syntax-Kapitel kennst:

svelte +page.svelte
<script>
    let { data } = $props();
</script>

<header>
    <h1>Hallo, {data.user.name}</h1>
</header>

{#await data.stats}
    <p>Lade Statistik …</p>
{:then stats}
    <article>
        <p>Aufträge: {stats.aufträge}</p>
        <p>Umsatz: {stats.umsatz} €</p>
    </article>
{:catch error}
    <p>Konnte Statistik nicht laden.</p>
{/await}

Was der Nutzer sieht:

  1. Sofort — der Header mit Username (sub-100ms).
  2. Darunter: „Lade Statistik …”.
  3. Nach 2 Sekunden: Die Statistik füllt sich, der Lade-Text verschwindet.

Drei Vorteile auf einen Schlag: Keine wahrgenommene Wartezeit, klare Trennung zwischen schnell und langsam, Fehler in der Statistik macht die Seite nicht kaputt.

Streaming nur in Server-Loads

Streaming funktioniert nur in +page.server.ts und +layout.server.ts. Universal-Loads (+page.ts) können keine ungewarteten Promises zurückgeben — dort musst du komplett auf Daten warten.

Warum? Universal-Loads laufen im Browser bei Client-Navigation. Dort gibt es kein Streaming-Pendant — fetch liefert ohnehin ein einzelnes Promise zurück. Das ungewartete-Promise-Pattern lohnt sich nur beim initialen Server-Render, wenn der Server HTML in Stücken streamen kann.

Mehrere Streams parallel

Du kannst beliebig viele Promises gleichzeitig streamen — jedes wird unabhängig aufgelöst, jedes hat seinen eigenen {#await}-Block.

ts Mehrere parallele Streams
export const load: PageServerLoad = async () => {
    return {
        schnell: {
            titel: 'Dashboard',
            nutzerCount: 42,
        },
        stats: computeStats(),       // 2s
        aktivitaet: getRecentActivity(), // 1s
        trends: computeTrends(),     // 4s
    };
};
svelte Drei unabhängige Streams
<script>
    let { data } = $props();
</script>

<h1>{data.schnell.titel}</h1>

<section>
    <h2>Statistik</h2>
    {#await data.stats}<p>…</p>{:then s}<p>{s.summary}</p>{/await}
</section>

<section>
    <h2>Aktivität</h2>
    {#await data.aktivitaet}<p>…</p>{:then a}<ul>{#each a as e}<li>{e}</li>{/each}</ul>{/await}
</section>

<section>
    <h2>Trends</h2>
    {#await data.trends}<p>…</p>{:then t}<chart data={t} />{/await}
</section>

Ergebnis: Header sofort, danach füllen sich die drei Sektionen unabhängig — Aktivität nach 1s, Stats nach 2s, Trends nach 4s. Der Nutzer sieht stetig wachsenden Content.

Wann lohnt sich Streaming, wann nicht?

Streaming ist nützlich, wenn:

  • Mindestens ein Teil der Daten schnell verfügbar ist (Header, Navigation, User-Info).
  • Mindestens ein Teil deutlich länger dauert (komplexe Datenbank-Queries, externe APIs).
  • Die langsamen Daten nicht überall benötigt werden (sondern in einem klaren Bereich).

Streaming lohnt sich nicht, wenn:

  • Alle Daten ungefähr gleich schnell kommen — der Aufwand von Streaming ist dann reine Komplikation.
  • Die Seite ohne die langsamen Daten gar keinen Sinn ergibt — dann müsste der Nutzer ohnehin warten.
  • Du eine prerender-Route hast — Prerender braucht alle Daten zur Build-Zeit, Streaming widerspricht dem Prinzip.

Was im Hintergrund passiert

Damit du dem Pattern vertrauen kannst, hier ein Blick auf die Mechanik. Beim Streaming geschieht Folgendes:

  1. Die load-Funktion läuft, gibt Daten und ungewartete Promises zurück.
  2. SvelteKit rendert das Markup mit einem Platzhalter für jedes ungewartete Promise.
  3. Der Server schickt das HTML — bis zu dem Punkt, an dem das Promise sitzt.
  4. Im Hintergrund wartet der Server auf das Promise-Ergebnis.
  5. Sobald es da ist, schickt der Server einen kleinen JavaScript-Schnipsel in die laufende Verbindung, der den Platzhalter mit dem echten Wert ersetzt.
  6. Die Verbindung wird geschlossen, wenn alle Promises aufgelöst sind.

Im Netzwerk-Tab sieht das aus wie ein einziger HTTP-Request mit Chunked Transfer Encoding. Der Browser interpretiert das nach und nach.

Ein Bonus: Dieses Verfahren funktioniert auch bei deaktiviertem JavaScript, wenn das Hosting Chunked Transfer unterstützt. Der Browser zeigt dann zwar den Lade-Text dauerhaft, sieht aber alles andere normal — keine Fehlerseite.

Streaming und Fehlerbehandlung

Wenn ein gestreamtes Promise mit einem Fehler endet, fängt der {:catch}-Zweig im {#await}-Block. Der Rest der Seite ist davon unbeeinflusst — eine kaputte Statistik macht den Header nicht kaputt.

svelte Mit Fehler-Zweig
{#await data.stats}
    <p>Lade …</p>
{:then stats}
    <p>{stats.summary}</p>
{:catch error}
    <p style="color: red">Statistik nicht verfügbar.</p>
{/await}

Das ist ein deutlicher Vorteil gegenüber „alle Daten in einer Promise.all-Wartung” — bei Streaming machen Einzel-Fehler nicht die ganze Seite kaputt.

Dynamic Imports plus Streaming für Bundle-Splitting

Wenn die Anzeige der gestreamten Daten ihre eigene schwere Komponente braucht (z. B. eine Chart-Library, die 200 KB groß ist), kannst du sie nur dann laden, wenn die Daten ankommen — per dynamischem Import.

svelte Lazy Chart
<script>
    let { data } = $props();

    // Komponente nur bei Bedarf nachladen
    const Chart = $derived(import('$lib/components/Chart.svelte'));
</script>

{#await data.trends}
    <p>Lade Trends …</p>
{:then trends}
    {#await Chart then mod}
        <mod.default data={trends} />
    {/await}
{/await}

Doppeltes {#await}: erst auf die Daten warten, dann auf die Komponente. So ist die Chart-Library nicht im Initial-Bundle und wird nur dann geladen, wenn der Nutzer die Daten braucht. Der Header und alle anderen Seiteninhalte sind sofort da.

Interessantes über Streaming

Streaming respektiert Cookies und Headers, die schon gesetzt sind. Beim ersten Byte des Streams gehen schon alle Cookies und Headers raus — du kannst danach keine mehr ändern. Cookie-Setzen muss vor return aus dem Load passieren.

Der erste sichtbare Inhalt zählt — gemessen mit FCP. Der First Contentful Paint (FCP), eine wichtige Web-Vital-Metrik, profitiert massiv von Streaming. Selbst wenn die ganze Seite langsam fertig ist, ist FCP schnell — und genau das wertet Google.

Streaming widerspricht Caching. Eine gestreamte Antwort lässt sich schwer cachen, weil sie pro Request unterschiedlich lange offen ist. Bei Inhalten, die sowieso öffentlich sind und gleich aussehen, ist klassisches Caching mit Cache-Control oft schneller als Streaming.

Edge-Functions können Streaming. Cloudflare Workers, Vercel Edge Functions und Co. unterstützen Streaming. Du kannst SvelteKit-Streaming also auch im Edge-Setup nutzen — sehr effektiv für Apps mit globalen Nutzern.

Streaming und SEO sind kompatibel. Search-Engine-Bots warten beim Crawlen, bis der Stream zu Ende ist. Sie sehen dann das vollständige HTML inklusive der gestreamten Inhalte. Du verlierst keinen SEO-Wert.

Im Browser-DevTools sieht Streaming wie ein langer einzelner Request aus. Der Network-Tab zeigt einen einzigen HTTP-Request, der mehrere Sekunden offen ist. Wer das nicht erwartet, sucht den fehlenden Datentransport an der falschen Stelle.

Du kannst nicht mitten im Stream redirecten. Sobald die Antwort gestartet ist, ist die URL festgeschrieben. Wer in einem gestreamten Promise auf einen Auth-Fehler stößt, kann nicht mehr zur Login-Seite umleiten — er kann nur einen Fehler in den Stream packen. Auth-Prüfungen gehören also vor das ungewartete Promise.

Bei langsamen Verbindungen kann Streaming sogar Vorteile gegen Server-side-Daten-Bündel haben. Wer ein langsames Netz hat (mobil, Zug-Wifi) bekommt mit Streaming sukzessiv Inhalte. Bei Bundle-Datenübertragung würde er nichts sehen, bis alles da ist.

Weiterführende Ressourcen

Externe Quellen

Verwandte Artikel

/ Weiter

Zurück zu SvelteKit Rendering

Zur Übersicht