Rendu côté serveur Svelte 5 (Runes)
Ce que couvre cette compétence
« SSR » signifie que chaque requête est rendue en HTML par un serveur (Node, Bun, serverless ou edge) avant que le navigateur ne la reçoive ; les mêmes composants hydratent ensuite l'application interactive. Contrairement à une construction uniquement statique, un vrai serveur existe au moment de la requête, donc +page.server.js, +layout.server.js, +server.js, les actions de formulaire et hooks.server.js sont autorisés — et c'est là que les données et les secrets doivent être placés.
Le côté « runes » est ce qui rend le SSR délicat : chaque composant s'exécute deux fois, une fois sur le serveur et une fois dans le navigateur. Un code parfaitement valide dans une application client seul se brise ici :
$effectne s'exécute jamais sur le serveur, donc tout état qu'il initialise est absent du HTML serveur.- Les
$stateau niveau module dans les fichiers.svelte.tssont créés une fois par processus serveur et partagés entre toutes les requêtes des utilisateurs. - Les valeurs calculées à partir de
Date.now(),Math.random()oulocalStoragependant le rendu diffèrent entre serveur et client, provoquant des décalages d'hydratation.
Ces trois échecs représentent la majorité des bugs SSR dans le code utilisant les runes, et chacun a un petit modèle correct ci-dessous.
Étape 1 — Choisir la forme du projet
| Besoin | Utiliser |
|---|---|
| Application complète : routes, chargement de données serveur, formulaires, auth, streaming | SvelteKit avec adapter-auto (détection automatique Vercel/Netlify/Cloudflare), adapter-node (auto-hébergement), ou un adaptateur de plateforme. C'est le défaut ; l'utiliser sauf s'il y a une raison de ne pas le faire. |
| Un arbre de composants Svelte dans un backend existant (Express/Hono/Fastify/Bun) | Vite + Svelte 5 simple : render() de svelte/server côté serveur, hydrate() dans le navigateur. Vous gérez le routage, les en-têtes et le traitement des requêtes. |
| Contenu majoritairement statique avec quelques routes dynamiques | SvelteKit avec export const prerender = true par route (ou 'auto' pour pré-rendre les pages populaires et rendre côté serveur la longue traîne). Les routes dynamiques suivent toujours chaque règle ici. |
Ne définissez pas export const ssr = false pour « corriger » un bug SSR — cela transforme la route en une coque rendue côté client (une SPA), perdant le SEO et le premier affichage. Ce n'est légitime que pour les pages qui ne peuvent vraiment pas se rendre sans globaux navigateur. csr = false est le commutateur inverse : HTML serveur sans hydratation, pour les pages qui n'ont besoin d'aucune interactivité.
Les étapes de configuration pour les deux chemins, ainsi que les adaptateurs et les options de page, se trouvent dans references/ssr-build.md. Lisez-le avant de générer svelte.config.js / vite.config.js, car les noms d'options et le comportement des adaptateurs diffèrent selon les versions et sont faciles à mal comprendre subtilement.
Étape 2 — Connaître les runes qui s'exécutent sur le serveur
Ce tableau est le cœur de la compétence :
| Rune | Pendant le SSR | Notes |
|---|---|---|
$state(v) | S'exécute | La valeur initiale est rendue. La muter en cours de rendu ne re-rend pas le HTML déjà émis — le SSR est un passage unique. |
$state.raw(v) | S'exécute | Pas de proxy. Bon pour les grandes charges data que vous remplacez en bloc plutôt que de les muter. |
$derived(expr) | S'exécute | Évalué dans le cadre du rendu, donc sa sortie atterrit dans le HTML. Gardez-le pur et sans API navigateur uniquement. |
$effect / $effect.pre | Jamais | Navigateur uniquement. L'état qu'il définit est absent du HTML SSR (apparition après hydratation). Ce n'est pas une erreur d'hydratation, mais c'est du HTML vide et un décalage de mise en page visible. |
$effect.root(fn) | Jamais | Idem. Ne construisez pas d'état visible par le serveur à l'intérieur. |
$props() / $bindable() | S'exécute | Les données chargées arrivent comme props ; les valeurs initiales sont rendues. |
$props.id() | S'exécute | ID unique compatible SSR, cohérent entre serveur et client (Svelte ≥ 5.20). Utilisez-le pour les liens for/aria-labelledby. |
$inspect(v) | Dev uniquement | Pas de sortie SSR. |
$state.snapshot(v) | S'exécute | Utilisez avant de passer l'état à une API non Svelte ou une bibliothèque serveur. |
$effect.tracking() | false | Le compilateur serveur le remplace par le littéral false ; il n'y a pas de contexte de tracking pendant le SSR. |
Le bug SSR classique que ce tableau évite :
<!-- ❌ vide dans le HTML serveur, puis apparaît après hydratation -->
<script>
let user = $state();
$effect(() => { user = getUser(); });
</script>
<div>{user?.name}</div>
La correction est presque toujours la même : calculer les valeurs visibles par le SSR dans $derived, ou les récupérer dans load et les passer comme props. Gardez $effect pour les choses qui n'existent que dans le navigateur (canvas, observateurs, analytique, synchronisation de stockage).
Lisez references/runes-ssr.md pour le comportement serveur complet rune par rune, les modèles de valeurs compatibles avec l'hydratation et le statut expérimental du SSR asynchrone.
Étape 3 — Garder l'état par requête, jamais global au module
// ❌ un seul objet partagé pour tout le processus serveur — l'utilisateur A peut voir les données de l'utilisateur B
// src/lib/user.svelte.ts
export const user = $state({ name: '' });
La correction canonique est de retourner les données par requête depuis load et de les transmettre comme props ; lorsque de nombreux composants imbriqués en ont besoin, placez-les dans un contexte avec un accesseur :
<!-- +layout.svelte -->
<script>
import { setContext } from 'svelte';
let { data } = $props();
setContext('user', () => data.user);
</script>
<!-- tout enfant -->
<script>
import { getContext } from 'svelte';
const user = getContext('user');
</script>
<p>Bonjour {user().name}</p>
Le contexte est attaché à l'arbre des composants, et chaque requête SSR obtient son propre arbre, donc il ne peut pas fuir entre utilisateurs. Passez une fonction (ou un objet avec des accesseurs) dans setContext, pas la valeur elle-même, sinon la réactivité est perdue. Les données par requête dont de nombreuses fonctions serveur ont besoin peuvent vivre sur event.locals, renseignées dans hooks.server.ts.
L'état au niveau module reste acceptable pour l'état d'application côté client uniquement qu'aucune donnée utilisateur ne touche (thème, indicateurs de fonctionnalité), tant que rien ne rend côté serveur des valeurs personnalisées à partir de celui-ci.
Étape 4 — Traiter l'hydratation comme un contrat
L'hydratation rejoue le HTML serveur ; le premier rendu du client doit correspondre. Toute valeur différente provoque une hydration_mismatch, qui n'est délibérément pas suppressible — corrigez le code, pas l'avertissement.
- Rendez de manière déterministe : pas de
Date.now(), pas de formatagenew Date(), pas deMath.random(), pas decrypto.randomUUID(), pas de lectureslocalStorage/window/navigatordans le chemin de rendu. - Générez des IDs avec
$props.id(), pas avec des compteurs aléatoires ou modulaires. - Formatez les dates sur le serveur dans
loadet passez des chaînes, ou passez toujours untimeZoneexplicite — les fuseaux horaires du serveur et du client diffèrent. - Valeurs côté client uniquement (stockage, requêtes média, géolocalisation) : commencez par une valeur par défaut compatible serveur et remplissez dans
$effect/onMount, ou conditionnez le balisage avec un indicateurmountedavec une solution de repli stable côté serveur.
<script>
import { browser } from '$app/environment';
let theme = $state('light');
$effect(() => {
if (!browser) return;
theme = localStorage.getItem('theme') ?? 'light'; // après montage, donc l'hydratation correspond
});
</script>
En SSR Svelte simple (sans SvelteKit), $app/environment n'est pas disponible — utilisez typeof window !== 'undefined' dans un effet, jamais dans le chemin de rendu.
Étape 5 — Charger et diffuser des données sur le serveur
+page.server.ts/+layout.server.tspour les bases de données, les fichiers et les variables d'environnement privées ;+page.tsuniversel pour les API publiques qui peuvent s'exécuter dans les deux environnements.- La fonction
loadserveur doit retourner des données qui survivent à la sérialisation devalue : JSON plusDate,Map,Set,BigInt,RegExpet les références cycliques. Pas de fonctions ni d'instances de classe. - Données lentes, non essentielles : retournez la promesse et utilisez
{#await}dans la page — les promesses imbriquées sont diffusées au fur et à mesure de leur résolution. Attendez les données essentielles avant de retourner pour qu'elles soient dans le premier affichage. - Utilisez
event.fetchdansload: il hérite des cookies de la requête et intègre les réponses pour l'hydratation au lieu de les re-récupérer. - Après la navigation,
datase met à jour sans recréer le composant : dérivez-en ($derived) au lieu de les copier dans des variables locales non réactives.
Le SSR asynchrone (await dans les composants) et les fonctions distantes sont encore expérimentaux à partir de Svelte 5.57 / SvelteKit 2.7x (septembre 2026). N'activez pas compilerOptions.experimental.async dans le code de production sauf si je le demande explicitement ; le statut exact et l'option d'activation se trouvent dans references/runes-ssr.md.
Étape 6 — Appliquer les règles de sécurité SSR
- Les secrets restent privés.
$env/static/private/$env/dynamic/privatesont réservés au serveur ; seules les variables préfixées parPUBLIC_atteignent le navigateur. Les imports depuis$lib/server/et*.server.jslèvent une erreur si un composant les importe. - Tout ce qui est retourné par
loadest intégré dans la charge utile HTML — intégralement, pas seulement les champs que vous rendez. Ne retournez jamais de mots de passe hashés, jetons ou champs internes dont vous n'avez pas besoin côté client. - CSP avec nonces. Configurez
kit.csp; SvelteKit injecte un nonce dans ses propres scripts en ligne, et%sveltekit.nonce%couvre les balises manuelles dansapp.html. Un vrai serveur peut envoyer de vrais en-têtes CSP — utilisez cela au lieu d'une balise<meta>plus faible. {@html}reste une XSS. Être rendu côté serveur ne signifie pas être assaini. Assainissez le HTML non fiable avec DOMPurify immédiatement avant le rendu.- L'authentification appartient à
hooks.server.ts(remplissezlocals) plus des vérifications au niveau des routes. Les fonctionsloadde layout ne s'exécutent pas à chaque navigation côté client. - La protection CSRF est activée par défaut pour les soumissions de formulaire ; laissez-la activée, et vérifiez l'origine vous-même dans tout point de terminaison de mutation
+server.jspersonnalisé. - Cookies de session :
httpOnly,secureen production,sameSite : 'lax'au minimum. - Un
fetchcôté serveur d'URLs fournies par l'utilisateur est une SSRF. Autorisez les hôtes sur une liste blanche ; bloquez les IP privées et les points de terminaison de métadonnées cloud.
Liste de contrôle complète et code : references/security.md.
Auditer une application existante
Lorsque la tâche est d'auditer ou de déboguer plutôt que de construire, parcourez references/ssr-review.md : vérifications du mode de rendu, runes invisibles côté serveur, risques d'hydratation, état limité à la requête, exposition de la charge utile, vérifications de sécurité et simulations de pannes. Signalez les résultats sous forme fichier:ligne avec la panne de production que chacun cause.
Fichiers de référence
references/runes-ssr.md— comportement serveur rune par rune, modèles de sécurité pour l'hydratation, état limité à la requête, rendusvelte/server+hydratesimple, et statut du SSR asynchrone / fonctions distantes expérimental.references/ssr-build.md— configuration SvelteKit et adaptateurs, options de page (ssr/csr/prerender), hooks, actions de formulaire, streaming, SSR Vite simple, et notes de déploiement.references/ssr-review.md— liste de contrôle d'audit pour les applications SSR existantes : modes de rendu, runes invisibles côté serveur, risques d'hydratation, cadrage de l'état, exposition de la charge utile, vérifications de sécurité et simulations de pannes.references/security.md— secrets et variables d'environnement, nonces CSP, XSS, CSRF, gestion auth/session, SSRF, fuite d'erreurs, et une liste de contrôle avant mise en production.assets/— fichiers de démarrage copiables :svelte.config.js(SvelteKit avec CSP),hooks.server.ts(auth plus en-têtes de sécurité), etindex.html/entry-server.js/entry-client.jspour le SSR Vite simple (le trio est construit en CI partests/plain-ssr).