An exhaustive, production-grade masterclass from absolute zero to staff-level Svelte 5 and full-stack SvelteKit architecture.
Master modern Runes reactivity ($state, $derived, $effect), reusable Snippets ({#snippet}), universal .svelte.ts modules, server-side data loading, progressive enhancement form actions, and enterprise performance.
Getting Started • Runes Reactivity • Snippets & Templates • SvelteKit Fullstack • State & Migration • SSR & Performance • Staff Handbook
flowchart LR
S1["Stage 1: Compiler & Setup"] --> S2["Stage 2: Svelte 5 Runes ($state)"]
S2 --> S3["Stage 3: Snippets & Templates"]
S3 --> S4["Stage 4: SvelteKit 2 Full-Stack"]
S4 --> S5["Stage 5: State & Store Migration"]
S5 --> S6["Stage 6: SSR, Hydration & Tests"]
S6 --> S7["Stage 7: Staff Architect Handbook & Cheatsheet"]
| Stage | Domain Focus | Core Concepts & Production Outcomes |
|---|---|---|
| Stage 1 | Compiler Foundations & Setup | Compiler-first paradigm, zero virtual DOM overhead, DOM surgical updates, project scaffolding with Vite & TypeScript. |
| Stage 2 | Svelte 5 Runes Reactivity | Deep dive into Runes: $state, $derived, $derived.by, $effect, $effect.pre, $props(), universal reactivity in .svelte.ts. |
| Stage 3 | Components, Snippets & Control Flow | Deprecation of <slot>, first-class {#snippet} and {@render}, blocks {#if}, {#each (key)}, {#await}, modern event handlers onclick. |
| Stage 4 | SvelteKit 2 Full-Stack Engine | File-system routing, +page.server.ts loaders, progressive enhancement with use:enhance, form actions, hooks, SSR/SSG. |
| Stage 5 | Enterprise State & Migration | Migrating from Svelte 4 writable() to modern Runes class stores, Context API (setContext/getContext), built-in transitions & FLIP animations. |
| Stage 6 | Performance, Hydration & Testing | Fine-grained hydration, DOM tree tree-shaking, Unit testing with Vitest & @testing-library/svelte, End-to-End testing with Playwright. |
| Stage 7 | Staff Architect Interview Handbook | 40+ staff-level scenario Q&As, Runes vs Signals vs React Hooks benchmark, memory leak isolation, enterprise cheatsheet. |
- 1. Stage 1: Absolute Beginner Foundations & Svelte 5 Architecture
- 2. Stage 2: Svelte 5 Runes Reactivity Deep Dive
- 3. Stage 3: Components, Snippets & Modern Control Flow
- 4. Stage 4: SvelteKit 2 Full-Stack Architecture
- 5. Stage 5: Enterprise State Management & Stores Migration
- 6. Stage 6: Enterprise Performance, Hydration & Testing
- 7. Stage 7: Staff Frontend Architect Interview Handbook & Cheatsheet
- 🤝 Contributing
- 📄 License
Traditional UI libraries (React, early Vue) ship a runtime library that maintains a Virtual DOM in memory. On every state change, they reconstruct virtual trees, run reconciliation algorithms (diffing), and patch changes to the real DOM:
graph TD
subgraph VDOM ["Virtual DOM Overhead (React)"]
StateChange1["State Change"] --> CreateVDOM["Re-execute Component & Create Virtual DOM Tree"]
CreateVDOM --> Diffing["Diff Old VDOM vs New VDOM (O(N) reconciliation)"]
Diffing --> Commit1["Batch Commit Real DOM Updates"]
end
subgraph SvelteCompiler ["Svelte 5 Surgical Compilation"]
StateChange2["$state Mutation"] --> DirectSignal["Reactive Signal Graph Notified"]
DirectSignal --> SurgicalUpdate["Direct DOM textNode.data = newValue (Zero Diffing!)"]
end
Svelte converts your declarative components at compile time into tiny, highly optimized JavaScript code that directly targets the real DOM without diffing overhead or heavy runtime libraries.
Create a modern Svelte 5 application:
# Initialize modern Svelte 5 project
npx sv create my-app
# Select SvelteKit or standalone Svelte with TypeScript
cd my-app
npm install
npm run devIn Svelte 5, components cleanly unify logic, templates, and scoped styles in one file:
<script lang="ts">
// Runes logic executes here
let count = $state(0);
let double = $derived(count * 2);
function increment() {
count += 1;
}
</script>
<div class="counter-card">
<h3>Count: {count}</h3>
<p>Double: {double}</p>
<button onclick={increment}>Increment Counter</button>
</div>
<style>
/* Styles are strictly scoped to this component by the compiler */
.counter-card {
border: 1px solid #334155;
padding: 1.5rem;
border-radius: 0.75rem;
background: #0f172a;
color: #f8fafc;
}
button {
background: #ff3e00;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
cursor: pointer;
}
</style>In Svelte 5, reactivity is explicitly declared using Runes (special compiler keywords prefixed with $).
$state wraps objects, arrays, and primitives in a fine-grained reactive proxy:
<script lang="ts">
interface TodoItem {
id: string;
text: string;
done: boolean;
}
// Deep reactivity: mutating nested properties triggers surgical updates!
let todos = $state<TodoItem[]>([
{ id: "1", text: "Master Svelte 5 Runes", done: false }
]);
function toggleTodo(id: string) {
const item = todos.find(t => t.id === id);
if (item) {
item.done = !item.done; // Direct mutation is automatically tracked!
}
}
function addTodo(text: string) {
todos.push({ id: crypto.randomUUID(), text, done: false }); // Standard array mutation!
}
</script>$derived creates a reactive computation that only recalculates when its tracked dependencies change:
<script lang="ts">
let items = $state([10, 20, 30, 40]);
// Simple single-expression derived value
let total = $derived(items.reduce((acc, curr) => acc + curr, 0));
// Complex multi-statement computation with $derived.by
let stats = $derived.by(() => {
const sum = items.reduce((a, b) => a + b, 0);
const avg = sum / (items.length || 1);
const max = Math.max(...items);
return { sum, avg, max };
});
</script>$effect runs when its dependencies update. It automatically handles cleanup:
<script lang="ts">
let query = $state("");
// Runs automatically on mount and whenever `query` changes
$effect(() => {
const controller = new AbortController();
async function search() {
if (!query.trim()) return;
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal
});
const data = await res.json();
console.log("Search results:", data);
}
search();
// Cleanup function executes before the next run or on component unmount
return () => {
controller.abort();
};
});
</script>Svelte 5 completely eliminates the legacy export let prop syntax in favor of the type-safe $props() rune:
<!-- Child.svelte -->
<script lang="ts">
interface Props {
title: string;
subtitle?: string;
count?: number;
onReset: () => void;
}
// Destructure with default fallback values
let { title, subtitle = "Default Subtitle", count = 0, onReset }: Props = $props();
</script>
<header>
<h2>{title}</h2>
<p>{subtitle}</p>
<span>Current: {count}</span>
<button onclick={onReset}>Reset</button>
</header>In Svelte 4, reactivity was locked inside .svelte component files. Svelte 5 unlocks Universal Reactivity: you can write $state and $derived in regular TypeScript files (.svelte.ts)!
// src/lib/cart.svelte.ts
export class ShoppingCart {
items = $state<{ id: string; name: string; price: number; quantity: number }[]>([]);
totalItems = $derived(this.items.reduce((acc, i) => acc + i.quantity, 0));
totalPrice = $derived(this.items.reduce((acc, i) => acc + i.price * i.quantity, 0));
addItem(product: { id: string; name: string; price: number }) {
const existing = this.items.find(i => i.id === product.id);
if (existing) {
existing.quantity += 1;
} else {
this.items.push({ ...product, quantity: 1 });
}
}
removeItem(id: string) {
this.items = this.items.filter(i => i.id !== id);
}
}
// Export singleton or instantiate per user session
export const cart = new ShoppingCart();Svelte 5 completely deprecates legacy <slot> tags in favor of Snippets: first-class, parameterized template blocks that behave like functions:
<script lang="ts">
interface User {
id: string;
name: string;
role: string;
}
let users = $state<User[]>([
{ id: "1", name: "Alice", role: "Admin" },
{ id: "2", name: "Bob", role: "Developer" }
]);
</script>
<!-- 1. Define a snippet with typed parameters -->
{#snippet userBadge(user: User)}
<div class="badge">
<strong>{user.name}</strong>
<span class="role">{user.role}</span>
</div>
{/snippet}
<!-- 2. Render snippet repeatedly or pass as a prop to child components -->
<div class="user-list">
{#each users as u (u.id)}
{@render userBadge(u)}
{/each}
</div>src/routes/
├── +layout.svelte # Root layout wrapper (header, nav)
├── +page.svelte # Homepage UI (/)
├── +page.ts # Universal client/server data loader
├── about/
│ └── +page.svelte # /about page
└── dashboard/
├── +layout.svelte # Dashboard sub-layout
├── +page.svelte # /dashboard UI
├── +page.server.ts # Server-only loader & form actions
└── [id]/
└── +page.svelte # Dynamic route (/dashboard/123)
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/db';
import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ locals, cookies }) => {
const sessionToken = cookies.get('session_token');
if (!sessionToken) {
throw error(401, 'Unauthorized');
}
const user = await db.user.findFirst({ where: { token: sessionToken } });
const metrics = await db.analytics.getMetrics(user.id);
return {
user,
metrics
};
};// src/lib/stores/auth.svelte.ts
class AuthManager {
user = $state<{ id: string; email: string } | null>(null);
isLoading = $state(false);
isAuthenticated = $derived(this.user !== null);
async login(email: string, pass: string) {
this.isLoading = true;
try {
const res = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email, pass }) });
this.user = await res.json();
} finally {
this.isLoading = false;
}
}
logout() {
this.user = null;
}
}
export const auth = new AuthManager();Testing modern Svelte 5 components using @testing-library/svelte:
// Counter.test.ts
import { render, screen, fireEvent } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import Counter from './Counter.svelte';
describe('Counter Component', () => {
it('renders initial state and increments surgically', async () => {
render(Counter);
const heading = screen.getByRole('heading', { level: 3 });
expect(heading.textContent).toContain('Count: 0');
const button = screen.getByRole('button', { name: /increment/i });
await fireEvent.click(button);
expect(heading.textContent).toContain('Count: 1');
});
});| Svelte 5 Rune | Purpose | Usage / Syntax |
|---|---|---|
$state(init) |
Reactive state declaration (deep proxy) | let count = $state(0); |
$derived(expr) |
Memoized derived projection | let double = $derived(count * 2); |
$derived.by(fn) |
Complex multi-statement derived value | let stats = $derived.by(() => { ... }); |
$effect(fn) |
DOM synchronization effect with cleanup | $effect(() => { window.addEventListener(...); return () => ... }); |
$props() |
Strictly typed component inputs | let { title, active = false }: Props = $props(); |
{#snippet name(args)} |
Reusable parameterized template snippet | {#snippet row(item)}...{/snippet} |
{@render name(args)} |
Renders a declared snippet | {@render row(data)} |
Q1: Why did Svelte 5 introduce Runes ($state, $derived) rather than keeping Svelte 4's let assignment reactivity?
Answer: Svelte 4's reactivity relied on compiler instrumentation of variable assignments (
count += 1), which only worked inside top-level.sveltefiles. Passing state to external helper functions or classes caused reactivity to break without cumbersome Svelte stores (writable). Runes decouple reactivity from component boundaries, allowing fine-grained Signal-based reactivity across universal.svelte.tsmodules, classes, and plain functions.
Answer: React Hooks re-execute the entire component function on every render cycle and must obey strict "Rules of Hooks" (no hooks inside loops or conditionals). In contrast, Svelte 5 component bodies execute only once on initialization. Runes construct a persistent fine-grained dependency graph; when
$statemutates, only the specific DOM nodes subscribed to that property update, with zero virtual DOM overhead.
Answer:
<slot>was a web-components inspired abstraction with significant limitations: awkward scoped-slot typing (let:item), inability to render slots conditionally without mounting components, and inability to render the same slot multiple times. Snippets are first-class template closures that can accept typed parameters, be stored in variables, and be passed as ordinary props.
Answer: Svelte 5 links every
$effectto its parent reactive boundary or component lifecycle root. When the component unmounts, Svelte automatically tears down the entire effect dependency subtree and calls all registered cleanup functions, preventing orphaned subscriptions.
(36 additional Staff-level architecture scenarios detailed inside repository documentation...)
Contributions are welcomed! Fork the repository, create a feature branch, and submit a PR conforming to the unified curriculum architecture.
Released under the MIT License.