This blog covered Astro 5.0’s bet on shipping zero JavaScript by default, and the honest comparison there was Astro against a full meta-framework. Svelte 5 is solving a different problem entirely, and it’s worth understanding as its own thing rather than lumping it in with “frameworks that ship less JS”. Svelte was never a virtual-DOM framework to begin with – it’s a compiler – and Svelte 5’s headline feature, runes, changes how that compiler tracks reactivity at a fundamental level.
No virtual DOM, and now no implicit reactivity either
React and Vue both diff a virtual DOM tree to figure out what changed. Svelte has never done that – it compiles your component to imperative DOM-update instructions at build time, so there’s no diffing step at runtime at all. What Svelte 4 didn’t have was an explicit way to say “this value is reactive” outside of top-level component variables; reactivity was inferred from assignment syntax ($: labels, let declarations), which worked but broke down once you tried to share reactive logic between files.
Runes: reactivity as an explicit primitive
Svelte 5 replaces implicit, compiler-magic reactivity with runes – function-like symbols the compiler recognises anywhere, not just inside .svelte files:
// a plain .svelte.js file - not a component
export function createCounter() {
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => {
console.log(`count changed to ${count}`);
});
return {
get count() { return count; },
increment: () => count++,
};
}
$state declares a reactive value, $derived computes from other reactive values without manually re-running anything, and $effect reacts to changes with a side effect. Because these are just functions the compiler recognises, you can now put reactive logic in a plain .js or .ts module and import it into any component – something Svelte 4’s compiler-only reactivity genuinely couldn’t do cleanly.
Fine-grained updates, not tree diffing
Under the hood, runes are backed by signals – each $state value tracks exactly which parts of the DOM read it, and updates only those nodes directly when it changes. There’s no component re-render to reconcile, no tree to diff, just a targeted DOM write. For a list of a thousand rows where one row’s value changes, Svelte updates that one text node; a virtual-DOM framework still has to diff the surrounding tree to work out that’s all that changed, even if it’s fast at doing so.
Props and components under runes
<script>
let { title, count = 0 } = $props();
</script>
<h2>{title}</h2>
<button onclick={() => count++}>
Clicked {count} times
</button>
$props() replaces the old export let syntax for declaring component inputs, and event handlers are now plain properties (onclick) rather than the on:click directive – both changes aimed at making Svelte components look and behave more like plain JavaScript objects and functions, which is also what makes runes portable outside components in the first place.
Migrating from Svelte 4
- Run the official migration script:
npx sv migrate svelte-5– it converts mostlet/$:patterns to runes automatically - Check any shared reactive logic that lived in
.jsfiles with stores – these can often become plain rune-based functions instead, dropping the store subscription boilerplate - Svelte 5 still supports the old syntax in compatibility mode, so migration can happen file by file rather than all at once
Where it fits next to Astro and the meta-frameworks
Astro’s islands can already host Svelte components, so these two posts aren’t actually competing recommendations – a content-heavy Astro site can use Svelte 5 specifically for the handful of components that do need interactivity, getting Astro’s zero-JS-by-default shell and Svelte’s no-virtual-DOM efficiency for the parts that hydrate. For a fully application-like UI with deep client-side state, SvelteKit (Svelte 5’s own meta-framework) is the more direct alternative to Next.js worth comparing against, not Astro.

Leave a Reply
You must be logged in to post a comment.