Svelte 5 marque un tournant majeur dans l'écosystème JavaScript avec l'introduction des Runes - un nouveau système de réactivité qui rend le code plus explicite, performant, et prévisible. Finies les variables magiquement réactives : bienvenue dans l'ère de la réactivité contrôlée et typée.
Pourquoi Svelte 5 Change Tout
Les Limites de Svelte 3/4
Réactivité implicite problématique :
<!-- Svelte 4 - Ambiguïté -->
<script>
let count = 0; // Réactif ? Oui, mais pas évident
let doubled = count * 2; // Réactif ? NON (bug classique)
$: doubled = count * 2; // OK, mais syntaxe bizarre
</script>
<button on:click={() => count++}>
{count} - {doubled}
</button>
Problèmes :
- Impossible de distinguer variable réactive vs normale
$:cryptique pour les débutants- Réactivité cassée dans
.jsmodules - TypeScript galère avec
$:statements
Svelte 5 : Clarté et Puissance
<!-- Svelte 5 - Explicite et clair -->
<script>
let count = $state(0); // ✅ Réactif explicitement
let doubled = $derived(count * 2); // ✅ Computed clair
$effect(() => {
console.log(`Count is ${count}`); // ✅ Side effect explicite
});
</script>
<button onclick={() => count++}>
{count} - {doubled}
</button>
Avantages :
- Réactivité explicite (pas de magie)
- Fonctionne dans
.jsmodules - TypeScript first-class support
- Performances +20-40% vs Svelte 4
Les 5 Runes Essentielles
1. $state - État Réactif
Remplace : Variables let réactives de Svelte 4
<script>
// Primitives
let count = $state(0);
let name = $state("Alice");
// Objets (proxy réactif automatique)
let user = $state({
name: "Bob",
age: 30,
preferences: {
theme: "dark"
}
});
// Arrays
let todos = $state([
{ id: 1, text: "Learn Svelte 5", done: false }
]);
function addTodo() {
// Mutations directes fonctionnent !
todos.push({ id: Date.now(), text: "New todo", done: false });
}
function updateTheme() {
// Nested reactivity fonctionne
user.preferences.theme = "light";
}
</script>
<button onclick={() => count++}>{count}</button>
<button onclick={addTodo}>Add Todo</button>
{#each todos as todo}
<label>
<input type="checkbox" bind:checked={todo.done} />
{todo.text}
</label>
{/each}
Deep Reactivity par défaut :
<script>
let state = $state({
user: {
profile: {
address: {
city: "Paris"
}
}
}
});
// ✅ Déclenche réactivité même en profondeur
state.user.profile.address.city = "Lyon";
</script>
Comparaison React :
// React - Immutabilité requise
const [user, setUser] = useState({ name: "Bob", age: 30 });
// ❌ Ne fonctionne pas
user.age = 31;
// ✅ Doit recréer objet
setUser({ ...user, age: 31 });
// Svelte 5 - Mutations directes
let user = $state({ name: "Bob", age: 30 });
user.age = 31; // ✅ Fonctionne !
2. $derived - Valeurs Calculées
Remplace : $: statements de Svelte 4
<script>
let width = $state(10);
let height = $state(5);
// Simple derived
let area = $derived(width * height);
// Derived complexe
let dimensions = $derived({
area: width * height,
perimeter: 2 * (width + height),
isSquare: width === height
});
// Chaining
let areaText = $derived(`Area: ${area}m²`);
</script>
<input type="number" bind:value={width} />
<input type="number" bind:value={height} />
<p>{areaText}</p>
<p>Perimeter: {dimensions.perimeter}m</p>
<p>{dimensions.isSquare ? "Square" : "Rectangle"}</p>
Derived avec logique :
<script>
let todos = $state([
{ id: 1, text: "Task 1", done: false },
{ id: 2, text: "Task 2", done: true }
]);
let stats = $derived({
total: todos.length,
completed: todos.filter(t => t.done).length,
remaining: todos.filter(t => !t.done).length,
progress: todos.length ? (todos.filter(t => t.done).length / todos.length) * 100 : 0
});
</script>
<p>Progress: {stats.progress.toFixed(0)}%</p>
<p>{stats.completed} / {stats.total} tasks completed</p>
Optimisation automatique :
- Recalculé seulement si dépendances changent
- Mémoisation automatique
- Pas de risque de boucles infinies
3. $effect - Side Effects
Remplace : $: statements avec side effects, onMount, afterUpdate
<script>
let count = $state(0);
// Effect basique
$effect(() => {
console.log(`Count changed to ${count}`);
});
// Effect avec cleanup
$effect(() => {
const interval = setInterval(() => {
count++;
}, 1000);
// Cleanup automatique au démontage
return () => clearInterval(interval);
});
// Effect conditionnel
$effect(() => {
if (count plus de 10) {
alert("Count exceeded 10!");
}
});
</script>
localStorage sync :
<script>
let theme = $state(localStorage.getItem("theme") ?? "light");
// Sync vers localStorage
$effect(() => {
localStorage.setItem("theme", theme);
});
// Sync depuis storage events
$effect(() => {
const handler = (e) => {
if (e.key === "theme") theme = e.newValue;
};
window.addEventListener("storage", handler);
return () => window.removeEventListener("storage", handler);
});
</script>
<button onclick={() => theme = theme === "light" ? "dark" : "light"}>
Toggle Theme ({theme})
</button>
API fetch avec cleanup :
<script>
let userId = $state(1);
let user = $state(null);
let loading = $state(false);
$effect(() => {
const controller = new AbortController();
loading = true;
fetch(`https://api.example.com/users/${userId}`, {
signal: controller.signal
})
.then(r => r.json())
.then(data => {
user = data;
loading = false;
})
.catch(err => {
if (err.name !== "AbortError") {
console.error(err);
loading = false;
}
});
// Cancel si userId change avant fin requête
return () => controller.abort();
});
</script>
{#if loading}
<p>Loading...</p>
{:else if user}
<p>{user.name}</p>
{/if}
4. $props - Props Typées
Remplace : export let de Svelte 4
<!-- Button.svelte -->
<script>
// Svelte 4 (ambigu)
export let label;
export let variant = "primary"; // Default value
</script>
<!-- Svelte 5 (explicite) -->
<script>
let { label, variant = "primary", onclick } = $props();
</script>
<button class={variant} {onclick}>
{label}
</button>
Props avec TypeScript :
<script lang="ts">
interface Props {
label: string;
variant?: "primary" | "secondary" | "danger";
disabled?: boolean;
onclick?: () => void;
}
let {
label,
variant = "primary",
disabled = false,
onclick
}: Props = $props();
</script>
<button
class={variant}
{disabled}
{onclick}
>
{label}
</button>
Rest props :
<script>
let { label, ...restProps } = $props();
</script>
<button {...restProps}>
{label}
</button>
<!-- Usage -->
<Button
label="Click me"
class="custom"
data-testid="btn"
aria-label="Action button"
/>
<!-- Tous les props non-destructurés passent via ...restProps -->
5. $bindable - Two-Way Binding
Nouveau : Props bindables explicitement
<!-- Counter.svelte -->
<script>
let { count = $bindable(0) } = $props();
</script>
<button onclick={() => count++}>
Count: {count}
</button>
<!-- App.svelte -->
<script>
let value = $state(0);
</script>
<Counter bind:count={value} />
<p>Parent sees: {value}</p>
Form input component :
<!-- TextInput.svelte -->
<script>
let {
value = $bindable(""),
label,
error
} = $props();
</script>
<label>
{label}
<input type="text" bind:value />
{#if error}
<span class="error">{error}</span>
{/if}
</label>
<!-- Usage -->
<script>
let email = $state("");
let emailError = $derived(
email && !email.includes("@") ? "Invalid email" : null
);
</script>
<TextInput
bind:value={email}
label="Email"
error={emailError}
/>
Pourquoi c'est important :
- Explicite (vs bind magique Svelte 4)
- TypeScript friendly
- Performance (pas de proxy inutile)
Patterns Avancés
1. Custom Stores avec Runes
<!-- stores.svelte.ts -->
export function createCounter(initial = 0) {
let count = $state(initial);
let history = $state<number[]>([]);
return {
get count() { return count; },
get history() { return history; },
increment() {
count++;
history.push(count);
},
decrement() {
count--;
history.push(count);
},
reset() {
count = initial;
history = [];
}
};
}
<!-- App.svelte -->
<script>
import { createCounter } from "./stores.svelte";
const counter = createCounter(10);
</script>
<button onclick={counter.increment}>+</button>
<button onclick={counter.decrement}>-</button>
<button onclick={counter.reset}>Reset</button>
<p>Count: {counter.count}</p>
<p>History: {counter.history.join(", ")}</p>
2. Reactive Class
<script>
class Todo {
id = $state(crypto.randomUUID());
text = $state("");
done = $state(false);
constructor(text: string) {
this.text = text;
}
toggle() {
this.done = !this.done;
}
}
class TodoList {
todos = $state<Todo[]>([]);
stats = $derived({
total: this.todos.length,
completed: this.todos.filter(t => t.done).length
});
add(text: string) {
this.todos.push(new Todo(text));
}
remove(id: string) {
this.todos = this.todos.filter(t => t.id !== id);
}
}
const list = new TodoList();
</script>
<input
type="text"
onkeydown={(e) => {
if (e.key === "Enter") {
list.add(e.currentTarget.value);
e.currentTarget.value = "";
}
}}
/>
{#each list.todos as todo}
<div>
<input type="checkbox" bind:checked={todo.done} />
<span class:done={todo.done}>{todo.text}</span>
<button onclick={() => list.remove(todo.id)}>Delete</button>
</div>
{/each}
<p>{list.stats.completed} / {list.stats.total}</p>
3. Composable Logic (comme Vue)
<!-- useMouse.svelte.ts -->
export function useMouse() {
let x = $state(0);
let y = $state(0);
$effect(() => {
const handler = (e: MouseEvent) => {
x = e.clientX;
y = e.clientY;
};
window.addEventListener("mousemove", handler);
return () => window.removeEventListener("mousemove", handler);
});
return {
get x() { return x; },
get y() { return y; }
};
}
<!-- App.svelte -->
<script>
import { useMouse } from "./useMouse.svelte";
const mouse = useMouse();
</script>
<p>Mouse: {mouse.x}, {mouse.y}</p>
4. Async Derived State
<script>
let userId = $state(1);
// Dérivé asynchrone
let user = $derived.by(async () => {
const res = await fetch(`https://api.example.com/users/${userId}`);
return res.json();
});
</script>
{#await user}
<p>Loading user {userId}...</p>
{:then data}
<p>Name: {data.name}</p>
{:catch error}
<p>Error: {error.message}</p>
{/await}
<button onclick={() => userId++}>Next User</button>
Note : $derived.by permet logique complexe (pas juste expressions)
Migration depuis Svelte 4
Avant/Après Comparaison
Svelte 4 :
<script>
export let initialCount = 0;
let count = initialCount;
$: doubled = count * 2;
$: if (count plus de 10) alert("High count!");
$: {
console.log("Count changed");
document.title = `Count: ${count}`;
}
onMount(() => {
const interval = setInterval(() => count++, 1000);
return () => clearInterval(interval);
});
</script>
Svelte 5 :
<script>
let { initialCount = 0 } = $props();
let count = $state(initialCount);
let doubled = $derived(count * 2);
$effect(() => {
if (count plus de 10) alert("High count!");
});
$effect(() => {
console.log("Count changed");
document.title = `Count: ${count}`;
});
$effect(() => {
const interval = setInterval(() => count++, 1000);
return () => clearInterval(interval);
});
</script>
Guide Migration Automatisé
# Svelte fournit outil de migration
npx sv migrate svelte-5
# Options
npx sv migrate svelte-5 --force # Sans confirmation
npx sv migrate svelte-5 --dry-run # Preview changements
Changements automatiques :
let x→let x = $state()$: y = x * 2→let y = $derived(x * 2)export let prop→let { prop } = $props()onMount/afterUpdate→$effect
Revue manuelle requise :
- Side effects complexes
- Stores custom
- Context API
Performance : Svelte 5 vs Concurrence
Benchmarks JS Framework Benchmark
Temps création 1000 lignes (ms, moins = mieux) :
- Svelte 5 : 18.2ms ⭐
- Svelte 4 : 23.7ms
- Vue 3.4 : 25.1ms
- React 18 : 42.3ms
- Angular 17 : 48.9ms
Temps update partiel (ms) :
- Svelte 5 : 12.4ms ⭐
- Svelte 4 : 16.8ms
- Vue 3.4 : 18.2ms
- React 18 : 29.7ms
Bundle size (gzipped) :
- Svelte 5 : 2.1kb ⭐
- Vue 3 : 13.5kb
- React 18 : 42kb
Mémoire (MB) :
- Svelte 5 : 3.2MB ⭐
- Svelte 4 : 3.8MB
- Vue 3.4 : 4.1MB
- React 18 : 5.9MB
Pourquoi Svelte 5 est Plus Rapide
- Fine-Grained Reactivity ** :
<script>
let user = $state({ name: "Alice", age: 30 });
</script>
<!-- React re-render tout le composant si user change -->
<!-- Svelte 5 : met à jour SEULEMENT le DOM affecté -->
<p>{user.name}</p> <!-- Update si name change -->
<p>{user.age}</p> <!-- Update si age change -->
- No Virtual DOM ** :
- Pas de diffing algorithm coûteux
- Mises à jour DOM chirurgicales
- Moins d'allocations mémoire
- Compile-Time Optimizations ** :
<!-- Svelte compile vers JS vanilla optimisé -->
<button onclick={() => count++}>
{count}
</button>
<!-- Devient approximativement : -->
<script>
let count = signal(0);
button.onclick = () => {
count.value++;
p.textContent = count.value; // Direct DOM update
};
</script>
SvelteKit 2.0 + Svelte 5
Server-Side Rendering (SSR)
<!-- +page.server.ts -->
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ params }) => {
const res = await fetch(`https://api.example.com/posts/${params.id}`);
const post = await res.json();
return { post };
};
<!-- +page.svelte -->
<script>
let { data } = $props();
</script>
<h1>{data.post.title}</h1>
<p>{data.post.content}</p>
Hydration sélective :
<!-- Interactive island -->
<script>
let count = $state(0);
</script>
<!-- Partie statique (SSR seulement) -->
<header>
<h1>My Blog</h1>
</header>
<!-- Partie interactive (hydratée) -->
<button onclick={() => count++}>
{count} likes
</button>
Streaming SSR
<!-- +page.server.ts -->
export const load: PageServerLoad = async () => {
return {
// Données instantanées
meta: { title: "Products" },
// Promise streamée
products: fetch("https://api.example.com/products").then(r => r.json())
};
};
<!-- +page.svelte -->
<script>
let { data } = $props();
</script>
<svelte:head>
<title>{data.meta.title}</title>
</svelte:head>
{#await data.products}
<p>Loading products...</p>
{:then products}
{#each products as product}
<ProductCard {product} />
{/each}
{/await}
Résultat :
- HTML initial envoyé immédiatement (meta)
- Skeleton/loading affiché
- Données streamées dès disponibles
- DOM mis à jour sans reload
Écosystème et Outils
Librairies Compatibles Svelte 5
UI Components :
- Skeleton UI : Composants Tailwind + Svelte 5
- Carbon Components Svelte : IBM Design System
- Melt UI : Headless components
- shadcn-svelte : Port shadcn/ui pour Svelte
État Global :
- Nanostores : Tiny stores (300 bytes)
- Svelte 5 runes : Souvent suffisant !
Forms :
- Superforms : Type-safe forms + validation
- Formsnap : Zod integration
Animation :
- Motion : Framer Motion-like pour Svelte
- AutoAnimate : Auto-animations
Testing :
- Vitest : Unit tests
- Playwright : E2E tests
- Testing Library Svelte : Component tests
Tooling
// vite.config.ts
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [sveltekit()],
test: {
include: ["src/**/*.{test,spec}.{js,ts}"],
environment: "jsdom"
}
});
ESLint + Prettier :
npm i -D eslint-plugin-svelte prettier-plugin-svelte
// .eslintrc.json
{
"extends": ["plugin:svelte/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"extraFileExtensions": [".svelte"]
}
}
Cas d'Usage Idéaux pour Svelte 5
1. Dashboards Temps Réel
<script>
let metrics = $state({ cpu: 0, memory: 0, requests: 0 });
$effect(() => {
const ws = new WebSocket("wss://api.example.com/metrics");
ws.onmessage = (e) => {
// Mutation directe = réactivité
Object.assign(metrics, JSON.parse(e.data));
};
return () => ws.close();
});
let cpuStatus = $derived(
metrics.cpu plus de 80 ? "danger" : metrics.cpu plus de 60 ? "warning" : "ok"
);
</script>
<Gauge value={metrics.cpu} status={cpuStatus} />
Pourquoi Svelte :
- Fine-grained updates (pas de re-render complet)
- Performance critique pour dashboards
2. Applications E-Commerce
<script>
let cart = $state([]);
let products = $state([]);
let total = $derived(
cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
let itemCount = $derived(
cart.reduce((sum, item) => sum + item.quantity, 0)
);
function addToCart(product) {
const existing = cart.find(item => item.id === product.id);
if (existing) {
existing.quantity++;
} else {
cart.push({ ...product, quantity: 1 });
}
}
</script>
<ProductGrid {products} onAddToCart={addToCart} />
<CartBadge count={itemCount} />
<CartTotal {total} />
3. Jeux et Animations
<script>
let position = $state({ x: 0, y: 0 });
let velocity = $state({ x: 0, y: 0 });
const GRAVITY = 0.5;
const JUMP_FORCE = -10;
$effect(() => {
const gameLoop = setInterval(() => {
// Physics update
velocity.y += GRAVITY;
position.x += velocity.x;
position.y += velocity.y;
// Ground collision
if (position.y plus de 400) {
position.y = 400;
velocity.y = 0;
}
}, 1000 / 60); // 60 FPS
return () => clearInterval(gameLoop);
});
function jump() {
if (position.y === 400) velocity.y = JUMP_FORCE;
}
</script>
<canvas
width={800}
height={600}
onclick={jump}
>
<Player x={position.x} y={position.y} />
</canvas>
Articles connexes
Pour approfondir le sujet, consultez également ces articles :
- Svelte 6 Runes : Le Guide Complet du Nouveau Système de Réactivité
- React 19 stable : Server Components et Actions révolutionnent le développement web
- React 19 stable : Server Actions et optimisations révolutionnent le développement web
Conclusion : Faut-il Migrer vers Svelte 5 ?
✅ Oui, si :
- Nouveau projet (adoptez Svelte 5 direct)
- Performance critique (dashboards, jeux)
- Vous voulez TypeScript excellent
- Bundle size important (mobile)
- Équipe petite/moyenne (courbe apprentissage faible)
⏸️ Attendez, si :
- Projet Svelte 4 stable et fonctionnel
- Pas de pain points actuels
- Deadlines serrés (laissez mûrir écosystème)
❌ Non, si :
- Écosystème React nécessaire (ex: React Native)
- Équipe déjà experte React/Vue
- Besoins UI complexes (React a plus de libs)
Ressources pour Démarrer
Documentation :
Templates :
# Créer projet SvelteKit + Svelte 5
npm create svelte@latest my-app
# Options recommandées :
# - Skeleton project
# - TypeScript
# - ESLint + Prettier
# - Vitest
# - Playwright
cd my-app
npm install
npm run dev
Communauté :
Svelte 5 est la version la plus mature et performante de Svelte. Les Runes apportent enfin la clarté et la prévisibilité qui manquaient. C'est le moment parfait pour plonger !



