Bisher haben wir gelernt: +page.svelte ist eine Seite. Aber wo kommen die Daten her, die diese Seite anzeigt? Antwort: aus einer Funktion namens load. Sie wohnt in einer Datei +page.ts oder +page.server.ts direkt neben +page.svelte. SvelteKit ruft sie vor dem Rendern auf, sammelt das Ergebnis und gibt es als data-Prop in deine Komponente. Dieser Artikel zeigt, wie das im Detail funktioniert, was du im event-Argument hast und wo die Grenze zwischen Universal- und Server-Load wirklich liegt.

Das Grundprinzip

Eine load-Funktion ist eine ganz normale Funktion. Sie wird exportiert, sie kann async sein, sie gibt ein Objekt zurück. Was sie zurückgibt, landet als data in deiner Page-Komponente.

ts src/routes/produkte/+page.ts
export async function load() {
    const response = await fetch('https://api.example.com/produkte');
    const produkte = await response.json();
    return { produkte };
}
svelte src/routes/produkte/+page.svelte
<script>
    let { data } = $props();
</script>

<ul>
    {#each data.produkte as produkt}
        <li>{produkt.name}</li>
    {/each}
</ul>

Genau diese Trennung ist wichtig zu verstehen: Daten holen ist Sache der load-Funktion, Daten anzeigen ist Sache der Komponente. Die Komponente weiß nicht, woher die Daten kommen — sie bekommt sie nur. Damit ist sie schön testbar und wiederverwendbar.

Universal vs. Server: zwei Welten, eine Funktion

load-Funktionen gibt es in zwei Varianten — und der Unterschied ist wichtig, sobald du etwas anderes als eine öffentliche API ansprichst.

+page.ts läuft universal. Beim ersten Seitenaufruf läuft sie auf dem Server (für SSR), bei späteren Client-Navigationen läuft sie im Browser. Das ist okay für öffentliche APIs, bei denen es keine Rolle spielt, wer den Request stellt.

+page.server.ts läuft nur auf dem Server. Code in dieser Datei landet niemals im Browser-Bundle. Das ist die richtige Wahl, sobald du:

  • Eine Datenbank abfragst (das wäre vom Browser nicht möglich und würde Credentials leaken).
  • Mit Cookies oder Sessions arbeitest.
  • Einen API-Key verwendest, der nicht öffentlich sein soll.
  • Auf das Filesystem des Servers zugreifst.
ts src/routes/dashboard/+page.server.ts
import { db } from '$lib/server/db';
import { redirect } from '@sveltejs/kit';

export async function load({ locals }) {
    if (!locals.user) redirect(303, '/login');

    const auftraege = await db.auftrag.findMany({
        where: { userId: locals.user.id },
    });

    return { auftraege };
}

Hier passiert mehrere Dinge gleichzeitig: Die Auth-Prüfung, eine Datenbank-Abfrage und eine Redirect, falls der Nutzer nicht eingeloggt ist. All das gehört nicht in den Browser — daher +page.server.ts.

Was steckt im event-Argument?

Die load-Funktion bekommt ein Objekt namens event als Argument. Darin findest du alles, was SvelteKit über den aktuellen Request weiß. Du destrukturierst typischerweise nur das, was du brauchst.

EigenschaftWas sie liefert
paramsDie dynamischen Pfad-Parameter, z. B. { id: '42' }
urlDie ganze URL als URL-Objekt (mit searchParams, pathname, …)
fetchEine SvelteKit-spezielle fetch-Variante (siehe unten)
localsEigene Server-Daten aus dem handle-Hook (z. B. eingeloggter User)
cookiesLese- und Schreibzugriff auf Cookies (nur Server-Load)
setHeadersEigene HTTP-Header an den Response anhängen
parentDaten aus übergeordneten Layout-Loads
dependsManuelle Invalidierungs-Tags

Ein typischer Server-Load nutzt mehrere davon:

ts event-Argument im Einsatz
export async function load({ params, url, locals, fetch }) {
    const page = Number(url.searchParams.get('page') ?? '1');
    const userId = locals.user?.id;
    const post = await fetch(`/api/posts/${params.slug}?page=${page}`).then((r) => r.json());
    return { post, currentUser: userId };
}

SvelteKits eigene fetch-Funktion

In event.fetch steckt eine besondere Variante der Browser-Fetch-API. Sie sieht aus wie die normale, kann aber drei Dinge mehr:

  1. Sie nutzt direkt deine eigenen API-Routen. Wenn du fetch('/api/produkte') aufrufst, ruft SvelteKit beim Server-Load die zugehörige +server.ts direkt auf — ohne tatsächlich einen HTTP-Request zu machen. Das spart Latenz.
  2. Sie überträgt Cookies und Header weiter. Der Auth-Cookie des Nutzers wird mitgereicht, sodass authenticated API-Calls funktionieren.
  3. Sie ist kompatibel zu beiden Welten. Im Server-Load läuft sie als „interner Aufruf”, im Browser als normaler fetch — derselbe Code funktioniert beidseits.

Faustregel: In load-Funktionen immer event.fetch nutzen, nie das globale fetch. Das spart dir doppelte HTTP-Requests und sorgt für korrekte Cookie-Übergabe.

TypeScript automatisch typisieren mit $types

Wenn du TypeScript benutzt, fragt sich SvelteKit beim Build: „Welche Pfad-Parameter hat diese Route? Welcher Rückgabe-Typ kommt aus dem Load? Was bekommt die Komponente als data-Prop?” Die Antworten generiert es automatisch in eine virtuelle Datei namens $types, die du importieren kannst.

ts src/routes/produkte/[id]/+page.server.ts
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
import { db } from '$lib/server/db';

export const load: PageServerLoad = async ({ params }) => {
    const produkt = await db.produkt.findUnique({
        where: { id: params.id },
    });
    if (!produkt) error(404, 'Produkt nicht gefunden');
    return { produkt };
};

Was das bringt:

  • params.id ist als string typisiert — automatisch aus dem Ordner-Namen [id].
  • Der Rückgabewert wird auf seinen Typ gechecked — wenn du produkt zurückgibst, weiß die Page-Komponente Bescheid.

In der Komponente passt der Import dazu:

svelte +page.svelte
<script lang="ts">
    import type { PageData } from './$types';

    let { data }: { data: PageData } = $props();
</script>

<h1>{data.produkt.name}</h1>
<p>{data.produkt.description}</p>

Wenn du das produkt aus dem Load entfernst, zeigt TypeScript sofort einen Fehler in der Komponente — sehr nützlich beim Refactoring.

Server- und Universal-Load kombinieren

Du kannst beide Varianten gleichzeitig nutzen — und das ist sogar manchmal sinnvoll. Wenn beide existieren, läuft erst +page.server.ts, dann bekommt +page.ts deren Output als Eingabe.

ts +page.server.ts (Server-only-Daten)
import { db } from '$lib/server/db';

export async function load() {
    const internalData = await db.product.findMany();
    return { internalData };
}
ts +page.ts (öffentliche Daten dazu)
export async function load({ data, fetch }) {
    // data enthält das Ergebnis von +page.server.ts
    const exchangeRates = await fetch('https://api.example.com/rates').then((r) => r.json());
    return {
        ...data,
        exchangeRates,
    };
}

Wozu so eine Aufteilung? Bei der Client-Navigation läuft +page.server.ts neu (per HTTP-Call zurück zum Server), +page.ts läuft im Browser. Wenn die exchangeRates häufig wechseln, kann das im Browser per fetch schnell aktualisiert werden — ohne dass die teure Datenbank-Abfrage jedes Mal mitläuft. Sie wird ja per HTTP-Cache des Servers automatisch gecacht oder bewusst neu geholt.

In den meisten Apps reicht eine der beiden Varianten. Die Kombi ist eine Optimierung, keine Standard-Form.

Fehlerbehandlung im Load

Wirft deine Load-Funktion einen unerwarteten Fehler, zeigt SvelteKit eine Fehlerseite. Für erwartete Fälle (404, 401, 403) gibt es zwei spezielle Helfer:

ts error und redirect
import { error, redirect } from '@sveltejs/kit';

export async function load({ params, locals }) {
    const post = await db.post.findUnique({ where: { slug: params.slug } });

    // Nicht gefunden? 404 mit eigener Meldung
    if (!post) error(404, 'Post nicht gefunden');

    // Nicht eingeloggt? Auf Login-Seite umleiten
    if (post.private && !locals.user) redirect(303, '/login');

    return { post };
}

Beide Funktionen werfen intern — das heißt, der Code danach wird nicht ausgeführt. Du musst sie nicht returnen, kein throw davor schreiben.

error(status, message) produziert eine Fehlerseite mit dem Status-Code. redirect(status, url) macht eine 303-Weiterleitung — der Browser lädt die andere URL.

Streaming: nicht alles auf einmal warten

Manchmal hast du in einer Load-Funktion schnelle und langsame Daten. Der schnelle Teil sollte die Seite nicht blockieren. SvelteKit kann Promises streamen — du gibst sie ungewartet zurück, die Seite rendert mit dem fertigen Teil und das Markup zeigt einen Lade-Zustand für den Rest.

ts Streaming
export async function load({ fetch }) {
    const headerData = await fetch('/api/header').then((r) => r.json());

    // Nicht warten — als Promise zurückgeben
    const slowStats = fetch('/api/expensive-stats').then((r) => r.json());

    return {
        headerData,
        slowStats, // unawaited Promise!
    };
}

In der Komponente nutzt du dann den {#await}-Block:

svelte Streaming-Anzeige
<script>
    let { data } = $props();
</script>

<h1>{data.headerData.title}</h1>

{#await data.slowStats}
    <p>Lade Statistik …</p>
{:then stats}
    <p>{stats.summary}</p>
{/await}

So bekommt der Nutzer den Header sofort, die langsame Statistik ploppt nach. Praktisch für Dashboards mit gemischter Datenherkunft. Funktioniert nur in Server-Loads (+page.server.ts und +layout.server.ts).

Interessantes

Ein paar Details rund um load, die nicht offensichtlich sind, aber das Verständnis vertiefen:

Loads laufen parallel, nicht sequenziell. Wenn deine Seite ein Layout-Load und ein Page-Load hat, ruft SvelteKit beide gleichzeitig auf. Das ist eine wichtige Performance-Eigenschaft. Wer einen Load braucht, der vom anderen abhängt, nutzt await parent() — siehe Folgeartikel zu Page-Daten.

Im Browser werden Loads beim Hover schon vorbereitet. Wenn der Nutzer mit der Maus über einen Link fährt, startet SvelteKit unter Umständen schon den load-Call für die Zielseite. Beim tatsächlichen Klick ist die Seite oft schon bereit. Das nennt sich preloading und ist im Standard-Setup aktiv.

event.fetch ist nicht überall die beste Wahl. In sehr Performance-kritischen Server-Loads, in denen du eine externe API mit eigener Auth-Lib (z. B. Supabase, Stripe) abfragst, ist deren native SDK oft besser als fetch. Die SDK kümmert sich um Retries, Pagination und Auth — was du sonst selbst nachbauen müsstest.

Loads werden gecacht — bis sie invalidiert werden. Bei Client-Navigation merkt sich SvelteKit, was es schon geladen hat. Wer eine Seite zurück navigiert, sieht oft die alten Daten ohne neuen Request. Mit invalidate() oder invalidateAll() aus $app/navigation triggert man einen Re-Load gezielt.

Die parent()-Funktion ist async, aber nicht teuer. await parent() wartet auf die Layout-Loads dieser Route — die laufen sowieso parallel. Du blockierst dich nicht durch den Aufruf, du synchronisierst dich nur an dem Punkt, an dem du die Eltern-Daten brauchst.

Server-Load-Daten landen serialisiert im HTML. Beim ersten Render (SSR) packt SvelteKit deine return-Werte als JSON ins HTML, damit der Browser bei der Hydration nichts neu laden muss. Achtung: Daten, die nicht als JSON serialisierbar sind (Funktionen, Date-Objekte teilweise), führen zu Warnungen. SvelteKit nutzt eine eigene serialize-Lib, die ein paar mehr Typen kennt als reines JSON.stringify.

Loads dürfen nicht-deterministisch sein — sind es aber besser nicht. Wenn dein Load Math.random() nutzt, bekommt jeder Request andere Daten. Das funktioniert, sorgt aber für Hydration-Warnungen, weil Server- und Client-HTML auseinanderlaufen können. Faustregel: Lade in load deterministisch, mache zufällige Ausgaben in der Komponente per $state.

Weiterführende Ressourcen

Externe Quellen

Verwandte Artikel

/ Weiter

Zurück zu SvelteKit Routing & Loading

Zur Übersicht