Knowing the runtime model explains performance characteristics and the subtle rules of runes.
Svelte 5 combines a compiler with a small signal runtime.
The compiler turns markup into code that creates DOM by cloning a parsed <template> element once per component type, then grabs references to the dynamic nodes. Each dynamic part (a text node, an attribute, a block) gets its own small effect that updates exactly that node. There is no virtual DOM and no component-level re-render: when count changes, only the effects that read count run.
Runes compile to signal operations. let count = $state(0) becomes a source signal; reads become $.get(count) and writes $.set(count, v), which is why runes look like plain variables but behave reactively, and why the rules about exports and destructuring exist: the compiler can only rewrite accesses it can see. Proxies created for objects and arrays hold one signal per property, created lazily.
$derived is a lazy computed signal; $effect and template updates are effects. Svelte uses a push-pull model: a write marks dependents dirty (push), and deriveds recompute only when read (pull). Effects are scheduled and flushed in a microtask batch, so multiple writes cause one update. flushSync() forces a synchronous flush; tick() awaits the next one.
Dependencies are collected dynamically on every run, so only state read on the current branch is tracked. You can view the generated JavaScript in the playground's JS output tab.
Example
// What the compiler roughly produces for:
// <script> let count = $state(0); let double = $derived(count * 2); </script>
// <button onclick={() => count++}>{count} x2 = {double}</button>
// (Svelte 5.57 output, lightly trimmed; internals change between versions)
import * as $ from 'svelte/internal/client';
var root = $.from_html(`<button> </button>`); // parsed once, cloned per instance
export default function App($$anchor) {
let count = $.state(0); // source signal
let double = $.derived(() => $.get(count) * 2); // lazy computed signal
var button = root();
var text = $.only_child(button);
$.template_effect(() => // fine-grained effect for one text node
$.set_text(text, `${$.get(count) ?? ''} x2 = ${$.get(double) ?? ''}`));
$.delegated('click', button, () => $.update(count)); // delegated click handler
$.append($$anchor, button);
}
$.delegate(['click']);
// Runtime model: $.set marks dependents dirty (push), deriveds recompute when read (pull),
// and effects are flushed together in a microtask (flushSync() forces it).Remember
- Markup compiles to cloned DOM templates plus one small effect per dynamic part
- Runes compile to signal reads and writes (
$.get,$.set) - Push-pull: writes mark dirty, deriveds recompute on read, effects flush in a batch
Watch out for
- Assuming a component function re-runs on state change like React; in Svelte only the affected effects run.