17.1 Introduction to Svelte
Svelte is a compiler-based framework for building user interfaces. Unlike React or Vue, it doesn't include a runtime library in the bundle. Instead, all the magic happens at build time, converting your code into plain JavaScript. The output is pure DOM manipulation code — small and fast.
What is Svelte? The Compiler-Based Framework
Svelte was created by Rich Harris in 2016. The name means "slim, streamlined" — true to its goal of a framework with no unnecessary runtime overhead.
Key Differences
| React / Vue | Svelte | |
|---|---|---|
| How it works | Virtual DOM diffing at runtime | Generates DOM manipulation code at compile time |
| Bundle size | Includes framework runtime | Only compiled code |
| Reactivity | Requires APIs like useState, ref | Automatic reactivity via assignment |
| Learning curve | Must learn the API | Syntax close to HTML/CSS/JS |
The Compilation Process
Counter.svelte
↓ (svelte compiler)
Counter.js (pure JavaScript DOM manipulation code)
A Svelte file (.svelte) isn't just a text file. During the build, the Svelte compiler reads it and transforms it into highly optimized JavaScript.
Why No Virtual DOM?
React's Virtual DOM compares previous and new virtual trees on state changes (diffing), and applies minimal updates to the real DOM. This is faster than direct DOM manipulation but the comparison itself has a cost.
Svelte takes a different approach. Since it already knows at compile time which variables affect which DOM elements, there's no need to compare at runtime.
<!-- Counter.svelte -->
<script>
let count = 0;
function increment() {
count++; // This assignment triggers a DOM update
}
</script>
<button on:click={increment}>
Clicks: {count}
</button>
After compilation, this roughly becomes:
// Simplified compiled output
function create_fragment(ctx) {
let button;
return {
c() {
button = element("button");
button.textContent = `Clicks: ${ctx[0]}`;
},
m(target, anchor) {
insert(target, button, anchor);
listen(button, "click", ctx[1]);
},
p(ctx, [dirty]) {
if (dirty & 1) { // Only when count changes
set_data(button_text, `Clicks: ${ctx[0]}`);
}
}
};
}
No virtual DOM diffing — only the text node is updated exactly when count changes.
Bundle Size Comparison: React / Vue / Angular vs Svelte
Approximate gzip bundle sizes for a simple counter app:
| Framework | Bundle Size (gzip) | Notes |
|---|---|---|
| Svelte | ~3 KB | No runtime, compiled code only |
| React + React DOM | ~45 KB | Includes virtual DOM runtime |
| Vue 3 | ~33 KB | Includes Composition API runtime |
| Angular | ~60 KB+ | Full framework |
Note: As an app grows, Svelte's compiled output also grows, narrowing the gap. Svelte's bundle size advantage is most pronounced in small to medium-sized apps.
Svelte 5 Key Changes: Introducing Runes
Svelte 5 (released late 2024) introduced the Runes system, fundamentally changing how reactivity is declared.
Svelte 4 vs Svelte 5
<!-- Svelte 4 -->
<script>
let count = 0; // Implicitly reactive
$: doubled = count * 2; // Reactive declaration
</script>
<p>Count: {count}, Doubled: {doubled}</p>
<!-- Svelte 5 (Runes) -->
<script>
let count = $state(0); // Explicit reactive state
let doubled = $derived(count * 2); // Explicit derived value
</script>
<p>Count: {count}, Doubled: {doubled}</p>
Svelte 5 Key Runes
| Rune | Role | Description |
|---|---|---|
$state() | Reactive state | Replaces implicit let reactivity |
$derived() | Derived value | Replaces $: reactive declarations |
$effect() | Side effects | Replaces $: { ... } blocks |
$props() | Component props | Replaces export let |
$bindable() | Two-way binding props | Allows bind: from parent |
Runes can be used in regular JavaScript files (.js, .ts), enabling reactive logic outside Svelte components.
When Svelte is a Good Fit
Good Use Cases
- Small to medium-sized apps: Bundle size advantage is maximized
- Performance-critical projects: No runtime overhead, fast initial loading
- Content-driven sites: SvelteKit's SSR/SSG support
- Low learning curve teams: Familiar HTML/CSS/JS-like syntax
- Embedded widgets: Can compile to standalone web components
Less Suitable Cases
- Large enterprise apps: Smaller ecosystem compared to React/Angular
- When rich third-party components are needed: React UI library ecosystem is larger
- When no Svelte-experienced team members exist: Consider hiring
Basic Component Example
A Svelte component has three sections:
<!-- Greeting.svelte -->
<script>
// JavaScript logic (Svelte 5: Runes)
let { name = 'World' } = $props();
let count = $state(0);
let message = $derived(`Hello, ${name}! (greeted ${count} times)`);
function greet() {
count++;
}
</script>
<!-- HTML template -->
<div class="greeting">
<h1>{message}</h1>
<button on:click={greet}>Say Hello</button>
</div>
<!-- Scoped styles (automatically component-scoped) -->
<style>
.greeting {
font-family: sans-serif;
padding: 1rem;
border: 2px solid #ff3e00;
border-radius: 8px;
}
button {
background-color: #ff3e00;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #dd3700;
}
</style>
Svelte File Structure
<script>: Component logic, Runes, imports- HTML template: Markup,
{expressions}, logic blocks <style>: CSS (component-scoped by default, class names are hashed)
Each section is optional and can appear in any order.
Todo App Example
<!-- TodoApp.svelte -->
<script>
let todos = $state([
{ id: 1, text: 'Learn Svelte', done: false },
{ id: 2, text: 'Learn SvelteKit', done: false },
{ id: 3, text: 'Master Runes', done: false },
]);
let newTodo = $state('');
let remaining = $derived(todos.filter(t => !t.done).length);
function addTodo() {
if (!newTodo.trim()) return;
todos.push({
id: Date.now(),
text: newTodo.trim(),
done: false,
});
newTodo = '';
}
function toggleTodo(id) {
const todo = todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
function removeTodo(id) {
todos = todos.filter(t => t.id !== id);
}
function handleKeydown(event) {
if (event.key === 'Enter') addTodo();
}
</script>
<div class="app">
<h1>Todo List <span class="badge">{remaining}</span></h1>
<div class="input-row">
<input
bind:value={newTodo}
on:keydown={handleKeydown}
placeholder="Add a new todo..."
/>
<button on:click={addTodo}>Add</button>
</div>
<ul>
{#each todos as todo (todo.id)}
<li class:done={todo.done}>
<input
type="checkbox"
checked={todo.done}
on:change={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button class="remove" on:click={() => removeTodo(todo.id)}>×</button>
</li>
{/each}
</ul>
{#if todos.length === 0}
<p class="empty">No todos. Add one above!</p>
{/if}
</div>
<style>
.app {
max-width: 400px;
margin: 2rem auto;
font-family: sans-serif;
}
.badge {
background: #ff3e00;
color: white;
border-radius: 999px;
padding: 0.1rem 0.5rem;
font-size: 0.8rem;
}
.input-row {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
input[type="text"], input:not([type]) {
flex: 1;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
ul { list-style: none; padding: 0; }
li {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-bottom: 1px solid #eee;
}
li.done span { text-decoration: line-through; color: #999; }
li span { flex: 1; }
.remove { background: none; border: none; color: #999; cursor: pointer; font-size: 1.2rem; }
.empty { text-align: center; color: #999; }
</style>
The Svelte REPL
Svelte provides an official online editor called the REPL (Read-Eval-Print Loop).
- URL: https://svelte.dev/repl
- Svelte 5 Playground: https://svelte.dev/playground
What you can do in the REPL:
- Run code and see results instantly
- View compiled JavaScript output (the "JS output" tab)
- Share examples (shareable URLs)
- Linked to official tutorials
REPL Tips
- Check the "JS output" tab to understand how Svelte works internally
- The "CSS output" tab shows scoped CSS after compilation
- Add multiple component files to test component communication
Pro Tips
Tip 1: Analyze Svelte's Compiled Output
To truly understand Svelte, always look at the compiled output:
# Bundle analysis
npm run build -- --report
# Bundle visualization with Vite
npx vite-bundle-visualizer
Tip 2: Using Svelte 4 and 5 Side by Side
A SvelteKit project can mix Svelte 4 and 5 syntax. Runes mode can be enabled per file, allowing gradual migration.
<!-- Force Svelte 5 Runes mode for this file -->
<svelte:options runes={true} />
<script>
let count = $state(0);
</script>
Tip 3: Compile to Web Components
Svelte components can be compiled to standard Web Components (Custom Elements):
<svelte:options customElement="my-counter" />
<script>
let count = $state(0);
</script>
<button on:click={() => count++}>Count: {count}</button>
<!-- Usable in any HTML file -->
<script src="./my-counter.js"></script>
<my-counter></my-counter>
Tip 4: Understanding Performance Benchmarks
Svelte consistently ranks near the top of js-framework-benchmark. However, "Svelte is always faster than React" is an overgeneralization. Real-world app performance depends more on architecture and implementation quality.
Summary
| Concept | Description |
|---|---|
| Compiler framework | Generates optimized JS at build time |
| No virtual DOM | Eliminates runtime diffing cost |
| Runes (Svelte 5) | $state, $derived, $effect, $props |
| Component structure | <script> + HTML + <style> |
| Scoped CSS | Styles apply only to the component |
| Bundle size | Significantly smaller than React/Vue |
The next chapter covers setting up your project with SvelteKit.