ormance gains are particularly relevant for large-scale applications with frequent state updates, where Svelte 5's signal-based approach minimizes unnecessary re-renders.
Core Solution
Svelte 5 runes are compiler macros that provide explicit control over reactivity. They function as JavaScript functions within <script> blocks but are transformed by the compiler into efficient update mechanisms. Runes are only valid in component script tags, module scripts, and rune functions.
1. $state: The Reactive Primitive
$state declares reactive variables. Unlike Svelte 4, where any variable could be reactive, Svelte 5 requires explicit declaration. $state is deep reactive by default, meaning nested object mutations trigger updates.
// Counter.svelte
<script lang="ts">
// Explicit reactive state
let count = $state(0);
let user = $state({ name: 'Alice', role: 'admin' });
// Deep mutation triggers update automatically
function updateUser() {
user.name = 'Bob';
}
// Reassignment updates reference
function resetCount() {
count = 0;
}
</script>
<button onclick={resetCount}>
Count: {count}
</button>
<button onclick={updateUser}>
Update User
</button>
Architecture Decision: Use $state for local component state. For performance-critical paths where deep reactivity is unnecessary, use $state.raw to prevent proxy overhead.
2. $derived: Lazy Computation
$derived creates values that automatically update when their dependencies change. Derived values are lazy; they only compute when accessed. This prevents expensive calculations during idle periods.
<script lang="ts">
let items = $state([1, 2, 3, 4, 5]);
// Computed lazily; updates only when 'items' changes
let total = $derived(items.reduce((sum, n) => sum + n, 0));
let hasItems = $derived(items.length > 0);
// Derived can depend on other derived values
let formattedTotal = $derived(`Total: ${total}`);
</script>
<p>{formattedTotal}</p>
<p>{hasItems ? 'List active' : 'Empty'}</p>
Rationale: $derived replaces reactive declarations ($:). It enforces purity; derived functions should not have side effects.
3. $effect: Side Effects
$effect runs code when dependencies change, after DOM updates. It replaces $: statements used for side effects. Effects automatically track dependencies and provide cleanup functions.
<script lang="ts">
let count = $state(0);
// Runs after DOM update when 'count' changes
$effect(() => {
console.log(`Count changed to ${count}`);
// Cleanup function runs before next effect execution
return () => {
console.log('Cleaning up previous effect');
};
});
// Effect with dependencies array (optional for explicit control)
$effect(() => {
// Runs once on mount and when count changes
}, [count]);
</script>
Key Behavior: $effect does not run during server-side rendering. It is strictly for client-side side effects.
4. $props: Component Interface
In Svelte 5, props must be declared using $props. Destructuring is mandatory to access reactive props. If you do not destructure, you receive the raw props object, which is not reactive.
<script lang="ts">
// Destructuring is required for reactivity
interface Props {
title: string;
count?: number;
onToggle?: () => void;
}
let {
title,
count = 0,
onToggle
}: Props = $props();
// $bindable enables two-way binding on props
let { value = $bindable('') }: { value?: string } = $props();
</script>
<h1>{title}</h1>
<p>{count}</p>
<input bind:value />
Critical Change: Svelte 5 removes the export let syntax. All prop declarations use $props. Default values are handled via destructuring defaults.
5. $inspect: Reactive Debugging
$inspect provides reactive debugging. It logs values whenever they change, similar to console.log but integrated with the reactivity system.
<script lang="ts">
let count = $state(0);
// Logs count whenever it changes
$inspect(count);
// Inspects deep changes with options
$inspect(count, { deep: true });
</script>
6. Module Context and Shared State
Runes can be used in <script module> blocks to create shared state across component instances. This replaces many use cases for Svelte stores.
<script module lang="ts">
// Shared state across all instances
let globalCount = $state(0);
export function incrementGlobal() {
globalCount++;
}
</script>
<script lang="ts">
// Component instance can access module state
console.log(globalCount);
</script>
Pitfall Guide
-
Forgetting to Destructure Props:
- Mistake: Using
let props = $props(); and accessing props.title.
- Result:
props is a reactive proxy, but accessing properties via dot notation does not trigger reactivity in templates or derived values.
- Fix: Always destructure:
let { title } = $props();.
-
Infinite Loops in $effect:
- Mistake: Writing to a state variable inside an
$effect that reads the same variable.
- Result: Infinite update loop.
- Fix: Ensure effects only write to state that they do not read, or use guards to prevent cycles.
-
Misusing $derived for Side Effects:
- Mistake: Performing API calls or mutations inside
$derived.
- Result: Derived values are lazy and may not run when expected, or run multiple times.
- Fix: Use
$effect for side effects; $derived is strictly for pure computations.
-
Assuming $state is Shallow:
- Mistake: Expecting
$state to behave like useState in React.
- Result: Mutating nested objects updates the UI, which may be unintended or cause performance issues with large objects.
- Fix: Use
$state.raw for large, immutable objects where deep reactivity is unnecessary.
-
Using Runes Outside Valid Context:
- Mistake: Calling runes inside regular functions or event handlers.
- Result: Compiler error or runtime failure. Runes are only valid in component script top-level or rune functions.
- Fix: Encapsulate rune logic in
$state, $derived, or $effect at the top level.
-
Store Migration Errors:
- Mistake: Attempting to use Svelte 4 store syntax (
$store) with Svelte 5 runes.
- Result: Runtime errors or stale data.
- Fix: Migrate stores to
$state in module context or use svelte/store interop carefully. Prefer runes for new state management.
-
Binding Syntax Confusion:
- Mistake: Using
bind:prop without $bindable in child components.
- Result: Two-way binding fails silently or throws errors.
- Fix: Mark props as
$bindable in the child component when using bind:prop in the parent.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Local Component State | $state | Simple, explicit, no boilerplate | Low |
| Computed Values | $derived | Lazy evaluation, automatic tracking | Low |
| Side Effects | $effect | Runs after DOM update, cleanup support | Medium |
| Shared State (Simple) | $state in <script module> | Replaces stores, type-safe | Low |
| Shared State (Complex) | Svelte Store or External Library | Advanced patterns, SSR support | Medium |
| Large Immutable Data | $state.raw | Reduces proxy overhead, better performance | Low |
| Two-Way Binding | $props with $bindable | Explicit opt-in, safer than default | Low |
Configuration Template
svelte.config.js
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter()
},
compilerOptions: {
runes: true // Enable runes mode explicitly
}
};
export default config;
vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
build: {
target: 'es2020',
sourcemap: true
}
});
Quick Start Guide
-
Create Project:
npm create svelte@latest my-svelte5-app
cd my-svelte5-app
npm install
-
Verify Runes Mode:
Ensure svelte.config.js includes runes: true in compiler options.
-
Create Reactive Component:
Create src/routes/+page.svelte:
<script lang="ts">
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => {
console.log(`Count is now ${count}`);
});
</script>
<h1>Svelte 5 Runes</h1>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onclick={() => count++}>Increment</button>
-
Run Development Server:
npm run dev
-
Inspect Output:
Verify console logs update on increment and UI reflects derived values. Check network tab for bundle size improvements.
Svelte 5 runes represent a mature evolution of Svelte's reactivity model. By embracing explicit state management, developers gain predictable behavior, improved performance, and a robust foundation for building scalable applications. The migration requires attention to prop handling and effect management, but the long-term benefits in maintainability and bundle efficiency justify the investment.