← Back
← Back to Writing

State Management at the Edge

State Management at the Edge

Svelte 5 introduced Runes — a set of compiler primitives that replace the store API with something fundamentally different. Instead of subscribing to external objects, you declare reactive values directly in your components. The compiler does the rest.

This isn’t just syntactic sugar. It changes where reactivity lives and how it propagates, which has real implications for edge-deployed applications where every kilobyte of runtime matters.

The reactive graph

Runes create a directed graph at compile time. Here’s a live visualization of how the three core primitives connect:

$state$derived$effect

$state is a source node — it holds a value and notifies dependents when it changes. $derived computes a value from one or more sources. $effect runs side effects when its dependencies update. The pulse traveling along each edge represents a reactive update propagating through the graph.

$state: the source of truth

A $state declaration tells the compiler to track reads and writes to a variable:

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

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

Under the hood, the compiler rewrites this into fine-grained signals. There’s no diffing, no proxy wrapper at runtime — the generated code directly invalidates the specific DOM text node that reads count. This is why Svelte 5 components ship less JavaScript than their Svelte 4 equivalents.

$derived: computed values without memoization boilerplate

$derived replaces $: reactive declarations with explicit dependency tracking:

<script>
  let width = $state(10);
  let height = $state(20);
  let area = $derived(width * height);
</script>

The key difference from Svelte 4’s $: is that $derived values are lazily evaluated and cached. If nothing reads area, the computation doesn’t run. If width changes but area isn’t currently rendered, it waits. This matters at the edge where compute budgets are tight.

For complex derivations, use the callback form:

<script>
  let items = $state([1, 2, 3]);
  let stats = $derived.by(() => {
    const sum = items.reduce((a, b) => a + b, 0);
    return { sum, avg: sum / items.length, count: items.length };
  });
</script>

$effect: controlled side effects

Effects run after the DOM updates, and the compiler automatically tracks which reactive values they read:

<script>
  let query = $state('');

  $effect(() => {
    const controller = new AbortController();
    fetch(`/api/search?q=${query}`, { signal: controller.signal });
    return () => controller.abort();
  });
</script>

The return function handles cleanup — when query changes, the previous fetch is aborted before the new one fires. This is the same pattern as React’s useEffect cleanup, but without the dependency array. The compiler figures out the dependencies.

Why this matters at the edge

Edge runtimes like Cloudflare Workers and Deno Deploy have strict constraints: cold start budgets under 5ms, memory limits, no long-lived process state. Runes help in three ways:

  1. Smaller bundles. No store runtime to ship. The reactive system is compiled away into direct variable assignments and targeted DOM updates.

  2. No runtime overhead. Svelte 4 stores used subscription objects allocated at runtime. Runes compile to static code paths — no allocations, no garbage collection pressure during renders.

  3. Predictable teardown. $effect cleanup functions run synchronously when a component is destroyed, which matters in edge environments where the runtime may terminate the isolate immediately after sending a response.

The shift from runtime reactivity (stores, proxies, virtual DOM diffing) to compile-time reactivity (signals wired by the compiler) is what makes Svelte 5 a natural fit for resource-constrained environments. You get the developer experience of writing reactive code with the production characteristics of hand-optimized imperative updates.