Was macht Svelte 5 Server-Side Rendering (Runes)?
Was dieser Skill abdeckt
„SSR” bedeutet, dass jede Anfrage von einem Server (Node, Bun, Serverless oder Edge) in HTML gerendert wird, bevor der Browser sie erhält; dieselben Komponenten hydrieren dann zu einer interaktiven App. Anders als bei einem rein statischen Build existiert zur Anfragezeit ein echter Server, daher sind +page.server.js, +layout.server.js, +server.js, Formularaktionen und hooks.server.js erlaubt – und genau dort gehören Daten und Geheimnisse hin.
Der Teil mit den Runes macht SSR knifflig: Jede Komponente läuft zweimal, einmal auf dem Server und einmal im Browser. Code, der in einer client-seitigen App einwandfrei funktioniert, bricht hier:
$effectwird auf dem Server nie ausgeführt, daher fehlt jeder von ihm gesetzte Zustand im Server-HTML.- Modulweites
$statein.svelte.ts-Dateien wird einmal pro Serverprozess erstellt und über alle Benutzeranfragen hinweg geteilt. - Werte, die während des Renderns aus
Date.now(),Math.random()oderlocalStorageberechnet werden, unterscheiden sich zwischen Server und Client, was zu Hydrierungsfehlern führt.
Diese drei Fehler sind für die meisten SSR-Bugs in Runes-Code verantwortlich, und jedes hat ein kleines, korrektes Muster weiter unten.
Schritt 1 – Wählen Sie die Projektform
| Bedarf | Verwendung |
|---|---|
| Vollständige App: Routen, serverseitiges Laden von Daten, Formulare, Authentifizierung, Streaming | SvelteKit mit adapter-auto (Vercel/Netlify/Cloudflare automatisch), adapter-node (selbst gehostet) oder einem Plattform-Adapter. Dies ist der Standard; verwenden Sie ihn, es sei denn, es gibt einen Grund, es nicht zu tun. |
| Ein Svelte-Komponentenbaum innerhalb eines bestehenden Backends (Express/Hono/Fastify/Bun) | Einfaches Vite + Svelte 5: render() von svelte/server auf dem Server, hydrate() im Browser. Sie kümmern sich um Routing, Header und Anfrageverarbeitung. |
| Größtenteils statische Inhalte mit wenigen dynamischen Routen | SvelteKit mit export const prerender = true pro Route (oder 'auto', um beliebte Seiten zu prerendern und den Langschwanz serverseitig zu rendern). Die dynamischen Routen folgen dennoch jeder hier stehenden Regel. |
Setzen Sie nicht export const ssr = false, um einen SSR-Fehler zu „beheben” – das verwandelt die Route in eine client-seitig gerenderte Hülle (eine SPA), wodurch SEO und der erste Paint verloren gehen. Es ist nur für Seiten legitim, die wirklich nicht ohne Browser-Globals gerendert werden können. csr = false ist der umgekehrte Schalter: Server-HTML ohne Hydrierung für Seiten, die keine Interaktivität benötigen.
Einrichtungsschritte für beide Pfade sowie Adapter und Seitenoptionen finden Sie in references/ssr-build.md. Lesen Sie es, bevor Sie svelte.config.js / vite.config.js generieren, da Optionsnamen und Adapterverhalten je nach Version unterschiedlich sind und leicht subtil falsch gemacht werden können.
Schritt 2 – Wissen, welche Runes auf dem Server laufen
Diese Tabelle ist der Kern des Skills:
| Rune | Während SSR | Anmerkungen |
|---|---|---|
$state(v) | Läuft | Der Anfangswert wird gerendert. Eine Mutation während des Renderns führt nicht zu einem erneuten Rendern des bereits ausgegebenen HTML – SSR ist ein Durchlauf. |
$state.raw(v) | Läuft | Kein Proxy. Gut für große data-Payloads, die Sie vollständig ersetzen, anstatt zu mutieren. |
$derived(expr) | Läuft | Wird als Teil des Renderns ausgewertet, sein Ergebnis landet daher im HTML. Halten Sie es rein und frei von Browser-Only-APIs. |
$effect / $effect.pre | Nie | Nur Browser. Der von ihm gesetzte Zustand fehlt im SSR-HTML (Pop-in nach Hydrierung). Dies ist kein Hydrierungsfehler, aber es ist leeres HTML und sichtbarer Layout-Shift. |
$effect.root(fn) | Nie | Gleich. Bauen Sie keinen server-sichtbaren Zustand darin auf. |
$props() / $bindable() | Läuft | Geladene Daten kommen als Props an; Anfangswerte rendern. |
$props.id() | Läuft | SSR-sichere eindeutige ID, konsistent zwischen Server und Client (Svelte ≥ 5.20). Verwenden Sie sie für for/aria-labelledby-Links. |
$inspect(v) | Nur Dev | Keine SSR-Ausgabe. |
$state.snapshot(v) | Läuft | Verwenden Sie es, bevor Sie den Zustand an eine Nicht-Svelte-API oder Serverbibliothek übergeben. |
$effect.tracking() | false | Der Server-Compiler ersetzt es mit dem Literal false; es gibt während SSR keinen Tracking-Kontext. |
Der klassische SSR-Fehler, den diese Tabelle verhindert:
<!-- ❌ leer im Server-HTML, taucht dann nach der Hydrierung auf -->
<script>
let user = $state();
$effect(() => { user = getUser(); });
</script>
<div>{user?.name}</div>
Die Behebung ist fast immer dieselbe: Berechnen Sie SSR-sichtbare Werte in $derived, oder holen Sie sie in load und übergeben Sie sie als Props. Behalten Sie $effect für Dinge, die nur im Browser existieren (Canvas, Observer, Analysen, Storage-Sync).
Lesen Sie references/runes-ssr.md für das vollständige Rune-für-Rune-Serververhalten, hydrierungssichere Wertemuster und den experimentellen asynchronen SSR-Status.
Schritt 3 – Zustand pro Anfrage, niemals modulglobal
// ❌ Ein gemeinsames Objekt für den gesamten Serverprozess – Benutzer A kann Daten von Benutzer B sehen
// src/lib/user.svelte.ts
export const user = $state({ name: '' });
Die kanonische Behebung besteht darin, pro-Anfrage-Daten aus load zurückzugeben und als Props nach unten zu reichen. Wenn viele verschachtelte Komponenten sie benötigen, legen Sie sie mit einem Getter in den Kontext:
<!-- +layout.svelte -->
<script>
import { setContext } from 'svelte';
let { data } = $props();
setContext('user', () => data.user);
</script>
<!-- jedes Kind -->
<script>
import { getContext } from 'svelte';
const user = getContext('user');
</script>
<p>Hi {user().name}</p>
Kontext ist am Komponentenbaum befestigt, und jede SSR-Anfrage erhält ihren eigenen Baum, daher kann er nicht zwischen Benutzern durchsickern. Übergeben Sie eine Funktion (oder ein Objekt mit Gettern) an setContext, nicht den Wert selbst, sonst geht die Reaktivität verloren. Pro-Anfrage-Daten, die viele Serverfunktionen benötigen, können auf event.locals leben, befüllt in hooks.server.ts.
Modulweiter Zustand ist weiterhin in Ordnung für rein client-seitigen App-Zustand, den keine Benutzerdaten berühren (Theme, Feature-Flags), solange nichts personalisierte Werte daraus serverseitig rendert.
Schritt 4 – Hydrierung als Vertrag behandeln
Hydrierung spielt das Server-HTML erneut ab; das erste Rendern des Clients muss damit übereinstimmen. Jeder abweichende Wert verursacht eine hydration_mismatch, die bewusst nicht unterdrückbar ist – beheben Sie den Code, nicht die Warnung.
- Rendern Sie deterministisch: kein
Date.now(), keinenew Date()-Formatierung, keinMath.random(), keincrypto.randomUUID(), keinelocalStorage/window/navigator-Lesevorgänge im Render-Pfad. - Generieren Sie IDs mit
$props.id(), nicht mit Zufälligkeit oder Modulzählern. - Formatieren Sie Daten auf dem Server in
loadund übergeben Sie Zeichenketten, oder übergeben Sie immer eine explizitetimeZone– Server- und Client-Zeitzonen unterscheiden sich. - Client-Only-Werte (Storage, Media Queries, Geolocation): Starten Sie mit einem server-sicheren Standardwert und füllen Sie ihn innerhalb von
$effect/onMountaus, oder schließen Sie Markup hinter einemmounted-Flag mit einem stabilen Server-Fallback.
<script>
import { browser } from '$app/environment';
let theme = $state('light');
$effect(() => {
if (!browser) return;
theme = localStorage.getItem('theme') ?? 'light'; // nach dem Mount, damit Hydrierung übereinstimmt
});
</script>
In einfachem Svelte SSR (ohne SvelteKit) ist $app/environment nicht verfügbar – verwenden Sie typeof window !== 'undefined' innerhalb eines Effekts, niemals im Render-Pfad.
Schritt 5 – Daten auf dem Server laden und streamen
+page.server.ts/+layout.server.tsfür Datenbanken, Dateien und private Umgebungsvariablen; universelles+page.tsfür öffentliche APIs, die an beiden Orten laufen können.- Server-
loadmuss Daten zurückgeben, die die Devalue-Serialisierung überstehen: JSON plusDate,Map,Set,BigInt,RegExpund zyklische Referenzen. Keine Funktionen oder Klasseninstanzen. - Langsame, nicht essentielle Daten: Geben Sie das Promise zurück und
{#await}es in der Seite – verschachtelte Promises streamen, während sie aufgelöst werden. Warten Sie essentielle Daten ab, bevor Sie zurückkehren, damit sie im ersten Paint enthalten sind. - Verwenden Sie
event.fetchinnerhalb vonload: Es erbt die Anfrage-Cookies und inlined Antworten für die Hydrierung anstatt erneut zu laden. - Nach der Navigation aktualisiert sich
data, ohne die Komponente neu zu erstellen: Leiten Sie davon ab ($derived), anstatt es in nicht-reaktive Locals zu kopieren.
Asynchrones SSR (await in Komponenten) und Remote-Funktionen sind ab Svelte 5.57 / SvelteKit 2.7x (September 2026) noch experimentell. Aktivieren Sie compilerOptions.experimental.async nicht in Produktionscode, es sei denn, ich frage explizit danach; der genaue Status und die Opt-In-Möglichkeit finden sich in references/runes-ssr.md.
Schritt 6 – Die SSR-Sicherheitsregeln anwenden
- Geheimnisse bleiben privat.
$env/static/private/$env/dynamic/privatesind nur-server; nur mitPUBLIC_-Präfix versehene Variablen erreichen den Browser. Importe von$lib/server/und*.server.jswerfen einen Fehler, wenn eine Komponente sie einbindet. - Alles, was von
loadzurückgegeben wird, ist im HTML-Payload eingebettet – alles, nicht nur die Felder, die Sie rendern. Geben Sie niemals Passwort-Hashes, Tokens oder interne Felder zurück, die Sie auf dem Client nicht benötigen. - CSP mit Nonces. Konfigurieren Sie
kit.csp; SvelteKit injiziert einen Nonce in seine eigenen Inline-Skripte, und%sveltekit.nonce%deckt manuelle Tags inapp.htmlab. Ein echter Server kann echte CSP-Header senden – verwenden Sie das anstelle eines schwächeren<meta>-Tags. {@html}ist immer noch XSS. Serverseitig gerendert bedeutet nicht bereinigt. Bereinigen Sie nicht vertrauenswürdiges HTML mit DOMPurify unmittelbar vor dem Rendern.- Authentifizierung gehört in
hooks.server.ts(befülltlocals) plus routenbezogene Prüfungen. Layout-load-Funktionen laufen nicht bei jeder client-seitigen Navigation. - CSRF-Schutz ist standardmäßig aktiviert für Formularübermittlungen; lassen Sie ihn aktiviert und prüfen Sie den Origin selbst in jedem benutzerdefinierten
+server.js-Mutationsendpunkt. - Session-Cookies:
httpOnly,securein Produktion,sameSite: 'lax'mindestens. - Server-seitiges
fetchvon benutzerbereitgestellten URLs ist SSRF. Erlauben Sie Hosts; blockieren Sie private IPs und Cloud-Metadaten-Endpunkte.
Vollständige Checkliste und Code: references/security.md.
Überprüfen einer bestehenden App
Wenn es um die Überprüfung oder Fehlersuche geht, nicht um den Bau, arbeiten Sie references/ssr-review.md durch: Rendering-Modus-Prüfungen, server-unsichtbare Runes, Hydrierungsrisiken, request-scoped State, Payload-Exposition, Sicherheits-Greps und Fehler-Durchläufe. Berichten Sie Ergebnisse als Datei:Zeile mit dem Produktionsfehler, den jeder verursacht.
Referenzdateien
references/runes-ssr.md– Rune-für-Rune-Serververhalten, Hydrierungssicherheitsmuster, request-scoped State, einfachessvelte/serverrender +hydrateund der experimentelle asynchrone SSR / Remote-Funktionen-Status.references/ssr-build.md– SvelteKit-Setup und Adapter, Seitenoptionen (ssr/csr/prerender), Hooks, Formularaktionen, Streaming, einfaches Vite SSR und Bereitstellungshinweise.references/ssr-review.md– Audit-Checkliste für bestehende SSR-Apps: Rendering-Modi, server-unsichtbare Runes, Hydrierungsrisiken, State-Scoping, Payload-Exposition, Sicherheits-Greps und Fehler-Durchläufe.references/security.md– Geheimnisse und Umgebungsvariablen, CSP-Nonces, XSS, CSRF, Auth/Session-Handling, SSRF, Fehlerlecks und eine Pre-Ship-Checkliste.assets/– kopierbare Startdateien:svelte.config.js(SvelteKit mit CSP),hooks.server.ts(Auth plus Sicherheitsheader) undindex.html/entry-server.js/entry-client.jsfür einfaches Vite SSR (das Trio wird in CI durchtests/plain-ssrgebaut).