Svelte 5: The Compiler-Driven Approach and the Mental Model
Imagine a JavaScript framework that completely disappears at build time. No libraries to download, no virtual DOM to be reconciled at runtime, no framework overhead visible in browser inspector. This and Svelte: not a framework in the traditional sense, but a compiler that transforms .svelte components in pure and optimized JavaScript. Svelte 5, released in October 2024, brings this model to a level up with Runes, a universal signal-based reactivity system that has revolutionized the world where you manage state in applications.
If you are coming from React, Vue or Angular, Svelte's mental model requires a change of perspective. It's not about it to learn a new API, but to understand that the framework itself does not exist at runtime. This guide helps you to build that mental model, exploring how the Svelte compiler works internally and why Runes represent the natural evolution of this approach.
What You Will Learn
- How the Svelte compiler transforms components into pure JavaScript
- Because Svelte doesn't need a virtual DOM
- The correct mental model for working with Svelte 5
- Introduction to Runes and signal-based reactivity
- Performance comparison with React and Vue (2025-2026 data)
- How to create the first Svelte 5 component with Runes
The Problem with Virtual DOMs
To understand why Svelte exists, you must first understand what it solves. React, Vue, and Angular all use a virtual DOM: An in-memory representation of the actual DOM that the framework uses to compute the differences (diffing) and apply only the necessary changes. This approach makes intuitive sense — and It's more efficient to update an in-memory representation than to manipulate the DOM directly — but it comes at a cost hidden: virtual DOM reconciliation always happens, even when nothing has changed.
Rich Harris, the creator of Svelte, coined the term "virtual DOM is pure overhead" in a 2019 article that sparked a lively debate in the community. The point is not that the virtual DOM is slow overall, but introduces unnecessary work that the compiler could eliminate.
Consider a React component that displays a counter:
// React: il virtual DOM riconcilia ad ogni render
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Contatore: {count}</p>
<button onClick={() => setCount(count + 1)}>
Incrementa
</button>
</div>
);
}
Every time the state changes, React must: Create a new virtual DOM for the component, compare it with the previous one (diffing), determine what has changed, and finally update the real DOM. The compiler Svelte already knows at build time what can change — just the text of the paragraph — and generates it directly code to update it, without any intermediate step.
How the Svelte Compiler Works
A Svelte component and a .svelte which contains three optional sections: script, template and
styles. During the build, the compiler parses this file and generates JavaScript that directly manipulates the
DOM, with granular and surgical updates.
<!-- Counter.svelte - Il componente sorgente -->
<script>
let count = $state(0);
</script>
<p>Contatore: {count}</p>
<button onclick={() => count++}>
Incrementa
</button>
The compiler analyzes the template and understands that {count} and the only part that changes.
The generated JavaScript directly contains: Create the node <p>, insert a text node,
and register an effect that only updates that text node when count change. No virtual DOM,
no diffing, no reconciliation.
The compiled code, simplified, looks like this:
// Output del compilatore Svelte (semplificato)
import { mount, text, element } from 'svelte/internal';
export function Counter(target) {
let count = 0;
const p = element('p');
const countText = text(`Contatore: ${count}`);
const button = element('button');
button.addEventListener('click', () => {
count++;
// Aggiorna SOLO il text node specifico
set_data(countText, `Contatore: ${count}`);
});
append(p, countText);
append(target, p);
append(target, button);
}
This is the heart of the compiler-driven approach: the compiler knows exactly what it can change and generates optimal code to handle it, instead of delegating this work to a generic diffing algorithm at runtime.
Bundle Size Comparison (2025)
An equivalent TodoList component produces JavaScript bundles of very different sizes across major frameworks:
- React 19 + ReactDOM: ~42 KB gzipped (runtime included)
- Vue 3.5: ~22 KB gzipped (runtime included)
- Angular 19 (standalone): ~35 KB gzipped (runtime included)
- Quick 5: ~2-5 KB gzipped (no runtime, just the component)
For large applications with many components, the advantage is reduced because each Svelte component includes its update code. But for medium-sized applications, Svelte remains the overall winner in terms of payload to the browser.
The Paradigm Shift of Svelte 5: The Runes
Svelte 4 used a magical reactivity system based on assignments: every variable in the block
<script> it was automatically reactive, and the compiler tracked dependencies throughout
the build. This worked well for simple cases, but had important limitations: Reactivity worked
only within .svelte components, and the magical behavior made the code difficult to reason about.
Svelte 5 introduces i Runes: special functions with the prefix $ who communicate
directly with the compiler. Runes are not normal functions — $state() it is not a function
that imports and calls — but special syntax recognized by the compiler, similar to how useState()
in React it is recognized by the transpiler for lint and type checks, but it is actually an ordinary function.
The key difference: Runes work everywhere, not just in .svelte files:
// counter.svelte.ts - Un modulo TypeScript puro con reattivita Svelte 5
export function createCounter(initial: number = 0) {
let count = $state(initial);
// $derived calcola automaticamente quando count cambia
const doubled = $derived(count * 2);
const isEven = $derived(count % 2 === 0);
function increment() { count++; }
function decrement() { count--; }
function reset() { count = initial; }
return {
get count() { return count; },
get doubled() { return doubled; },
get isEven() { return isEven; },
increment,
decrement,
reset
};
}
// Usabile in qualsiasi componente .svelte
// o in altri file TypeScript che importano questo modulo
This is a radical change compared to Svelte 4: the business logic with its reactivity can live in separate TypeScript modules, shareable between components, testable in isolation with normal unit tests, without dependencies on a rendering environment.
The Four Fundamental Runes
Svelte 5 introduces four main runes that cover most use cases:
<script lang="ts">
// $state: stato reattivo (sostituisce let reattivo di Svelte 4)
let name = $state('Federico');
let items = $state<string[]>([]);
// $derived: valori computati (sostituisce $: di Svelte 4)
const greeting = $derived(`Ciao, ${name}!`);
const itemCount = $derived(items.length);
// $effect: side effects (sostituisce $: con side effects di Svelte 4)
$effect(() => {
console.log('name cambiato:', name);
// Cleanup automatico quando l'effetto si ri-esegue
return () => console.log('cleanup prima del prossimo run');
});
// $props: props del componente (sostituisce export let di Svelte 4)
const { title, onClose = () => {} } = $props<{
title: string;
onClose?: () => void;
}>();
</script>
Svelte 4 vs Svelte 5: Key Differences
If you have experience with Svelte 4, the Runes change the syntax significantly:
- Quick 4:
let count = 0;(implicit magic) → Quick 5:let count = $state(0);(explicit) - Quick 4:
$: doubled = count * 2;→ Quick 5:const doubled = $derived(count * 2); - Quick 4:
export let prop;→ Quick 5:const { prop } = $props(); - Quick 4:
on:click={handler}→ Quick 5:onclick={handler}
Svelte 5 maintains backward compatibility with Svelte 4 in "legacy mode", so the migration can be gradual.
Svelte 5 vs React: A Practical Comparison
To concretely understand the difference in approach, let's see the same component implemented in React and Svelte 5:
// React 19: SearchBox con debounce
import { useState, useEffect, useCallback } from 'react';
function SearchBox({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('');
const [debouncedQuery, setDebouncedQuery] = useState('');
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedQuery(query);
}, 300);
return () => clearTimeout(timer);
}, [query]);
useEffect(() => {
if (debouncedQuery) onSearch(debouncedQuery);
}, [debouncedQuery, onSearch]);
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Cerca..."
/>
);
}
<!-- Svelte 5: SearchBox con debounce -->
<script lang="ts">
const { onSearch }: { onSearch: (q: string) => void } = $props();
let query = $state('');
$effect(() => {
const timer = setTimeout(() => {
if (query) onSearch(query);
}, 300);
return () => clearTimeout(timer);
});
</script>
<input bind:value={query} placeholder="Cerca..." />
The Svelte 5 version is more concise not because it is "magic": it is because the compiler manages synchronization
between query and input via bind:value, And $effect track automatically
dependence on query without having to explicitly declare it in the dependency array.
Real Performance: Data and Benchmark
Synthetic benchmarks are often misleading, but the data from JS Framework Benchmark 2025 (running on real hardware with intensive DOM operations) show Svelte 5 consistently in the top quartile for almost all tested operations:
- Creating 10,000 rows: Svelte 5 ~1.2x overhead vs Vanilla JS, React ~2.1x
- Update every 10th row: Svelte 5 ~1.1x, React ~1.8x
- Select row highlight: Svelte 5 ~1.05x, React ~1.4x
- Memory after creation: Svelte 5 uses ~40% less memory than React
The advantage is most pronounced on low-end mobile devices, where parsing and execution of JavaScript they cost more. On modern desktops, the practical difference for typical applications is often invisible to the end user.
Creating the First Quick Project 5
The quickest way to get started with Svelte 5 is to use SvelteKit, the official full-stack framework based on Svelte:
# Crea un nuovo progetto SvelteKit con Svelte 5
npm create svelte@latest my-svelte-app
cd my-svelte-app
# Seleziona: Skeleton project, TypeScript, ESLint, Prettier
npm install
# Avvia il dev server
npm run dev
Verify that the project uses Svelte 5 by checking the package.json:
{
"dependencies": {
"@sveltejs/kit": "^2.5.0",
"svelte": "^5.0.0"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"vite": "^5.0.0"
}
}
Create your first component with Runes in src/lib/components/Hello.svelte:
<script lang="ts">
// $props: tipo sicuro per le props del componente
const { name = 'Mondo' }: { name?: string } = $props();
// $state: stato locale reattivo
let clickCount = $state(0);
// $derived: valore computato da state e props
const message = $derived(
clickCount === 0
? `Ciao, ${name}!`
: `Ciao, ${name}! Hai cliccato ${clickCount} volt${clickCount === 1 ? 'a' : 'e'}`
);
</script>
<div class="hello">
<p>{message}</p>
<button onclick={() => clickCount++}>
Clicca qui
</button>
</div>
<style>
.hello {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 8px;
}
button {
margin-top: 0.5rem;
padding: 0.5rem 1rem;
}
</style>
SvelteKit: The Full-Stack Framework
Quick alone and only the UI layer. SvelteKit and the application framework built on Svelte which adds routing, SSR (Server-Side Rendering), SSG (Static Site Generation), form actions, and load functions for data fetching. And the equivalent of Next.js for React or Nuxt for Vue.
The structure of a SvelteKit project follows file-based conventions:
src/
├── routes/
│ ├── +layout.svelte # Layout globale
│ ├── +page.svelte # Homepage (/)
│ ├── about/
│ │ └── +page.svelte # Pagina /about
│ └── blog/
│ ├── +page.svelte # Lista articoli (/blog)
│ ├── +page.server.ts # Data fetching server-side
│ └── [slug]/
│ ├── +page.svelte # Articolo singolo (/blog/nome)
│ └── +page.server.ts
├── lib/
│ ├── components/ # Componenti condivisibili
│ └── utils/ # Utilita
└── app.html # Template HTML base
When to Choose Svelte 5
Svelte 5 is the optimal choice when:
- Upload performance is critical (mobile, slow connections)
- The bundle size must be minimum
- The team is willing to learn a mental model different from React
- You want an excellent DX (Developer Experience) with less boilerplate
- You build a content-heavy site that benefits from SSR/SSG with SvelteKit
Svelte 5 may NOT be the right choice when:
- The team has strong React experience and the migration would cost too much
- Many UI component libraries are sought (the React ecosystem is broader)
- It must integrate with libraries that take React (headless UI, Radix, etc.)
The Mental Model: What to Keep in Mind
Working with Svelte 5 requires internalizing some fundamental differences compared to frameworks based on virtual DOM:
- The compiler is your tool, not the runtime. When something doesn't work like you expect, think about how the compiler will interpret your code, not a rendering loop.
-
Runes communicate with the compiler.
$state(),$derived()and the others are not simple functions: they are instructions to the compiler on how to handle reactivity. -
Responsiveness is granular by design. Quickly updates only what is actually there
changed. There is no mechanism to manually optimize with
useMemoouseCallback. -
.svelte.ts files enable universal responsiveness. You can use Runes in any
file with extension
.svelte.tso.svelte.js, not just in the components.
Conclusions and Next Steps
Svelte 5 represents the most significant evolution of the framework since its creation. The approach compiler-driven isn't new, but Runes take signal-based responsiveness outside the confines of components, enabling more flexible and testable architectural patterns. The result is a framework that combines excellent performance, minimal bundle, and a developer experience free of useless boilerplate.
The next article in the series explores in depth $state and $derived: how it works
deep responsiveness via Proxy ES6, like $derived implement memoization, and how
use Runes in TypeScript files shared between components.
Series: Svelte 5 and Frontend Compiler-Driven
- Article 1 (this): Compiler-Driven Approach and Mental Model
- Article 2: $state and $derived — Universal Reactivity with Runes
- Article 3: $effect and the Lifecycle — When to Use It (and When Not)
- Article 4: SvelteKit SSR, Streaming and Load Functions
- Article 5: Transitions and Animations in Svelte 5
- Article 6: Accessibility in Svelte: Compiler Warnings and Best Practices
- Article 7: Global State Management: Context, Runes and Stores
- Article 8: Migrating from Svelte 4 to Svelte 5 — Practical Guide
- Article 9: Testing in Svelte 5: Vitest, Testing Library and Playwright







