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.
export async function load() {
const response = await fetch('https://api.example.com/produkte');
const produkte = await response.json();
return { produkte };
}<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.
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.
| Eigenschaft | Was sie liefert |
|---|---|
params | Die dynamischen Pfad-Parameter, z. B. { id: '42' } |
url | Die ganze URL als URL-Objekt (mit searchParams, pathname, …) |
fetch | Eine SvelteKit-spezielle fetch-Variante (siehe unten) |
locals | Eigene Server-Daten aus dem handle-Hook (z. B. eingeloggter User) |
cookies | Lese- und Schreibzugriff auf Cookies (nur Server-Load) |
setHeaders | Eigene HTTP-Header an den Response anhängen |
parent | Daten aus übergeordneten Layout-Loads |
depends | Manuelle Invalidierungs-Tags |
Ein typischer Server-Load nutzt mehrere davon:
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:
- Sie nutzt direkt deine eigenen API-Routen. Wenn du
fetch('/api/produkte')aufrufst, ruft SvelteKit beim Server-Load die zugehörige+server.tsdirekt auf — ohne tatsächlich einen HTTP-Request zu machen. Das spart Latenz. - Sie überträgt Cookies und Header weiter. Der Auth-Cookie des Nutzers wird mitgereicht, sodass authenticated API-Calls funktionieren.
- 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.
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.idist alsstringtypisiert — automatisch aus dem Ordner-Namen[id].- Der Rückgabewert wird auf seinen Typ gechecked — wenn du
produktzurückgibst, weiß die Page-Komponente Bescheid.
In der Komponente passt der Import dazu:
<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.
import { db } from '$lib/server/db';
export async function load() {
const internalData = await db.product.findMany();
return { internalData };
}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:
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.
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:
<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.