← Dashboard

Web Frameworks · Deep dive

Svelte

Deep dive · 12 min read

Svelte

Status: evaluated (deep dive) Last updated: 2026-06-16 One-line verdict: A compiler that turns components into lean, direct-DOM JavaScript — smallest footprint, simplest mental model, best DX scores — at the cost of a far smaller ecosystem and hiring pool than React.


Snapshot

What it is A compiler-first UI framework. Components are compiled at build time into small vanilla-JS that updates the DOM directly. There is no framework runtime / no Virtual DOM shipped.
Latest stable Svelte 5.55.x; SvelteKit 2.57.x (Svelte 5, with "runes", shipped Oct 2024).
Governance Independent open-source project led by Rich Harris, who works on it full-time at Vercel (since 2021). Single de-facto sponsor — smaller backing surface than React's foundation.
Model Compile-time reactivity. The compiler tracks which DOM nodes depend on which state and generates targeted update code — no diffing at runtime.
Language .svelte single-file components (HTML + <script> + <style>). TypeScript first-class.

The mental model

Where React re-runs your component and diffs the result, Svelte does the work at build time. The compiler reads your component, figures out exactly which DOM nodes depend on which pieces of state, and emits code that updates only those nodes when the state changes.

React:  state change → re-run component → diff VDOM → patch DOM   (runtime)
Svelte: state change → run the exact update for that node          (compiled)

Consequences:

  • No re-render model to manage. No dependency arrays, no useMemo/useCallback, no "why did this re-render." Your component <script> runs once; only reactive updates re-execute.
  • Minimal shipped code. No runtime to download — bundles start tiny.
  • The "magic" moved from runtime (React's diffing) to compile time, which is mostly invisible until you debug generated code or hit a compiler edge case.

The basics — a working handbook

Goal: after this section you can build a real component in Svelte 5. Everything here is the runes API (Svelte 5). If a tutorial shows export let, $:, or $store auto-subscription as the primary mechanism, it's Svelte 4 — recognise it and translate. (Runes are the thing AI tools most often get wrong; see the ecosystem section.)

The .svelte file

A component is one file with three optional parts — script, markup, styles:

<script>
  let name = $state("Homie");
</script>

<h1>Hello, {name}</h1>

<style>
  h1 { color: rebeccapurple; }   /* scoped to THIS component automatically */
</style>

{expr} interpolates any JS expression into markup. Styles are component-scoped by default — no CSS-in-JS, no naming conventions.

Runes, in one paragraph

Runes are compiler keywords that look like functions and start with $ ($state, $derived, $effect, $props, $bindable). They are not imported — the compiler recognises them. They replace Svelte 4's implicit "any top-level let is reactive" rule with something explicit and usable anywhere, including plain .svelte.js/.svelte.ts modules.

$state — reactive state

Declare reactive state with $state. Then mutate it directly — no setter, no immutability ceremony. The compiler wires up the DOM update.

<script>
  let count = $state(0);
</script>

<button onclick={() => count++}>{count}</button>

Deep reactivity: objects and arrays passed to $state become deep proxies, so mutating nested fields or using push/splice just works and updates the UI:

<script>
  let todos = $state([{ text: "ship", done: false }]);
  function add(text) { todos.push({ text, done: false }); }   // re-renders
  function toggle(t)  { t.done = !t.done; }                    // re-renders
</script>
  • For class instances, mark reactive fields with $state directly: class Box { size = $state(0); }.
  • Use $state.raw(obj) when you want a value that is replaced wholesale rather than deeply tracked (cheaper for large immutable data) — reassign it to update, don't mutate.

$derived — computed values

Values computed from state. They re-evaluate automatically when their inputs change, and only then. This is a first-class primitive — the equivalent of React's useMemo, but automatic and dependency-tracked.

<script>
  let items = $state([{ price: 10 }, { price: 5 }]);
  let total = $derived(items.reduce((s, i) => s + i.price, 0));
</script>

<p>Total: {total}</p>

For multi-statement logic, use $derived.by(() => { … }):

let summary = $derived.by(() => {
  const open = todos.filter(t => !t.done).length;
  return `${open} of ${todos.length} open`;
});

Rule: if a value can be computed from other state, use $derived — never recompute it manually inside an $effect.

$effect — side effects

For talking to the outside world (network, subscriptions, timers, manual DOM, logging) after the DOM updates. Dependencies are tracked automatically — whatever reactive values you read inside, it re-runs when they change. No dependency array (React's #1 bug class simply doesn't exist).

<script>
  let id = $state(1);
  let user = $state(null);

  $effect(() => {
    let active = true;
    fetch(`/api/users/${id}`)               // reading `id` registers it as a dependency
      .then(r => r.json())
      .then(u => { if (active) user = u; });
    return () => { active = false; };        // cleanup — runs before re-run / on destroy
  });
</script>

{#if user}<p>{user.name}</p>{:else}<p>Loading…</p>{/if}
  • $effect.pre(() => …) runs before the DOM updates (the rare case where you must read layout pre-update).
  • Don't use effects to derive state — that's what $derived is for. Effects are for genuine side effects only.

$props — component inputs

Inputs arrive via the $props() rune, destructured — with defaults and rest:

<!-- Button.svelte -->
<script>
  let { label = "OK", disabled = false, ...rest } = $props();
</script>

<button {disabled} {...rest}>{label}</button>

The {disabled} shorthand means disabled={disabled}. {...rest} spreads remaining attributes onto the element.

Two-way binding — bind: and $bindable

Svelte has first-class two-way binding, which React deliberately lacks (React uses one-way controlled inputs). Bind a form value to state directly:

<script>
  let name = $state("");
</script>

<input bind:value={name} />
<p>Hello, {name}</p>

To let a parent bind to a child's prop, mark it $bindable:

<!-- FancyInput.svelte -->
<script>
  let { value = $bindable() } = $props();
</script>
<input bind:value={value} />

<!-- Parent.svelte -->
<FancyInput bind:value={message} />

bind:this={el} binds a variable to the actual DOM node (Svelte's equivalent of React's useRef for elements):

<script>
  let input;
  function focus() { input.focus(); }
</script>
<input bind:this={input} />

Events

Event handlers are plain attributes taking a function — onclick, oninput, onsubmit (lowercase, native-style in Svelte 5):

<button onclick={() => count++}>+</button>
<form onsubmit={(e) => { e.preventDefault(); save(); }}>…</form>

Template logic — {#if}, {#each}, {#await}

Logic lives in the markup as blocks, not JS expressions:

{#if loading}
  <Spinner />
{:else if error}
  <p class="error">{error}</p>
{:else}
  {#each items as item (item.id)}     <!-- (item.id) = the key, for stable updates -->
    <Row {item} />
  {/each}
{/if}

{#await promise} handles promises inline — pending / resolved / rejected:

{#await fetchUser()}
  <p>Loading…</p>
{:then user}
  <p>{user.name}</p>
{:catch e}
  <p>{e.message}</p>
{/await}

{#key expr}…{/key} destroys and recreates its contents when expr changes (useful to replay a transition or reset state).

Snippets and {@render} — reusable markup (replaces slots)

Snippets are chunks of markup you can define and render, optionally with arguments. They replace Svelte 4 slots and are the equivalent of React's children / render-props.

{#snippet row(item)}
  <li>{item.name} — {item.price}</li>
{/snippet}

<ul>
  {#each items as item}{@render row(item)}{/each}
</ul>

Content passed between a component's tags is available as the children prop and rendered with {@render children()}:

<!-- Card.svelte -->
<script> let { children } = $props(); </script>
<div class="card">{@render children()}</div>

<!-- usage -->
<Card><p>Anything here becomes `children`.</p></Card>

Lifecycle — onMount, onDestroy

These are imported functions (not runes). onMount runs once after the component is first rendered; onDestroy on teardown. (Much of what needed lifecycle in Svelte 4 is now better expressed with $effect.)

<script>
  import { onMount } from "svelte";
  onMount(() => {
    const t = setInterval(tick, 1000);
    return () => clearInterval(t);   // cleanup, like $effect's return
  });
</script>

Context — share data down the tree

setContext/getContext pass data to descendants without prop-drilling (React's useContext equivalent). Set in an ancestor, read in any descendant:

<!-- ancestor -->
<script>
  import { setContext } from "svelte";
  setContext("theme", $state({ mode: "dark" }));
</script>

<!-- descendant -->
<script>
  import { getContext } from "svelte";
  const theme = getContext("theme");
</script>

Shared / global state — .svelte.js modules (replaces stores)

Because runes work outside components, the idiomatic way to share state across the app is a .svelte.js (or .svelte.ts) module that exports $state:

// counter.svelte.js
export const counter = $state({ count: 0 });
export function inc() { counter.count++; }
<script>
  import { counter, inc } from "./counter.svelte.js";
</script>
<button onclick={inc}>{counter.count}</button>

This largely replaces Svelte 4 stores (writable/readable, the $store auto-subscription syntax). Stores still exist and remain handy for streams/ RxJS-style flows, but for ordinary shared state, runes-in-a-module is the current idiom.

Why there's no $memo or useCallback

A frequent React question — "where's the Svelte equivalent of useMemo / useCallback / React.memo?" — has the same answer: you don't need one. Because the compiler already tracks exactly what depends on what, it never re-runs your component top-to-bottom, so there are no stale function identities to stabilise and no wasteful recomputation to memoise away. $derived covers the "cache a computed value" case automatically. This is the single biggest day-to-day ergonomics difference from React.

Debugging — $inspect

$inspect(value) logs a value and re-logs whenever it changes — a reactivity-aware console.log for development:

$inspect(count, user);   // logs on every change to either

That's the full working set. With $state, $derived, $effect, $props, bind:/$bindable, events, template blocks, snippets, lifecycle, context, and shared-state modules, you can build essentially any UI — with noticeably less ceremony than the React equivalent.


Routing and full-stack: SvelteKit

Background: meta-frameworks.md explains meta-frameworks and routing in general.

Svelte's official meta-framework is SvelteKit — and unlike React, there is essentially one answer, maintained by the same team. It bundles:

  • File-based routing, layouts, server endpoints
  • Server-side load functions, form actions, and remote functions
  • SSR / SSG / SPA / prerendering, selectable per route
  • One-command deploy adapters for Vercel, Netlify, Cloudflare, Node, static

Idiomatic data loading happens in a load function (server or universal), not in a component effect:

// +page.server.js
export async function load({ params }) {
  const user = await db.getUser(params.id);
  return { user };          // available to the page as `data.user`
}

Architecturally: "adopting Svelte" means adopting SvelteKit — far fewer stack decisions than React, less analysis paralysis, but also less optionality and a heavier dependence on one project's roadmap.

Rendering models

See web-app-concepts.md for what CSR / SSR / SSG / prerendering mean.

Via SvelteKit, Svelte covers the same spectrum as React — CSR, SSR, SSG, prerendering, configurable per route. It does not have a direct equivalent to React Server Components; instead it leans on load functions

  • server endpoints + the compiler's already-small output to keep client JS low. Simpler model, slightly less granular than RSC for streaming server-only component trees.

Tooling and ecosystem

This is Svelte's weak axis.

  • Smaller ecosystem. Fewer component libraries and integrations (Skeleton, Flowbite-Svelte, Melt UI, shadcn-svelte). Often one option where React has five; sometimes you wrap a vanilla-JS library yourself.
  • Excellent first-party DX — Vite-based, fast HMR, great error messages, built-in formatter/state/transitions reduce the need for third-party libs.
  • Less LLM / Stack Overflow depth — and Svelte 5 runes are recent enough that some AI assistance and tutorials still reference the older Svelte 4 syntax. Verify examples target runes.
  • No first-party native story comparable to React Native (NativeScript-Svelte exists but is niche).
  • TypeScript support is strong.

Performance characteristics

This is Svelte's decisive advantage.

  • Baseline bundle: a trivial app ships ~3–10 KB vs React's ~40–70 KB — no runtime to download. A static SvelteKit page can deliver under ~15 KB of JS.
  • On the Krausest js-framework-benchmark, Svelte 5 sits top-tier, beating React 19 by ~15–30% on create/update/memory in many runs.
  • Faster cold start / Time-to-Interactive on constrained devices and slow networks — the gap is largest for small-to-medium apps and shrinks (relatively) as app code dwarfs framework code.
  • Same reality check as React: for typical business apps, network and DB dominate. Svelte's edge matters most for low-end hardware, strict load budgets, or embedded/kiosk-style targets.

Strengths

  • Smallest footprint and best raw performance of the mainstream options.
  • Simplest mental model — no re-render/dependency-array class of bugs; consistently top developer-satisfaction scores in State of JS.
  • Batteries included — scoped styles, transitions, stores, routing (via Kit) without assembling a stack.
  • One coherent stack (SvelteKit) — fewer decisions, less churn surface.
  • Less code to write — components are typically shorter than React equivalents.

Weaknesses / risks

  • Smaller ecosystem and hiring pool — ~10x fewer jobs than React; fewer ready-made libraries; more "build it yourself."
  • Concentrated governance — effectively one lead at one sponsor (Vercel). Lower bus-factor / backing surface than React's multi-vendor foundation — a real concern for a 10-year system.
  • Recent breaking change — Svelte 5 runes were a significant rewrite from Svelte 4; ecosystem, docs, and AI assistance are still catching up.
  • Compile-time magic — debugging occasionally drops into generated code; fewer engineers can reason about the compiler internals.

How it ages

Technically clean — the compiled output is small and the runes model is explicit and stable-looking. The durability questions are organizational, not technical: ecosystem longevity and the bus-factor around a single-lead project. The Svelte 4→5 transition shows the team will make breaking changes (handled with a migration path, but real). For a long-lived system, the bet is less about the framework decaying and more about talent and library availability a decade out.


Fit for our context (.NET, long-lived, safety-adjacent)

Criterion Rating Notes
Longevity / governance 0/− Technically sound, but single-sponsor / single-lead is the weakest point for a 10-year horizon.
.NET / team fit 0 Pairs fine with an ASP.NET/Web API backend (SPA + JSON). We have less in-house Svelte experience than React — ramp cost.
Maintainability + Simpler model, less code, fewer footguns once learned — but smaller talent pool to maintain it.
Regulatory / safety fit 0 Clean, auditable, self-hostable output; but fewer validated libraries and a thinner support/talent base for regulated contexts.
Performance & footprint ++ Best in class — meaningful if any targets are constrained/embedded/kiosk devices.
Ecosystem & hiring The clear weak spot vs React.
Rendering / interactivity + Full spectrum via SvelteKit; no RSC equivalent but rarely needed.

Net: the technically superior, organizationally riskier option. If the deciding criteria are performance, footprint, DX, and long-term code simplicity, Svelte wins. If they are hiring, ecosystem, and backing stability — which weigh heavily for long-lived, safety-adjacent systems — the smaller talent pool and concentrated governance are the things to weigh against its clear technical merits.


Sources