Svelte State: When to Use Reactive Statements vs. Derived Stores
When a Svelte component’s <code>$:</code> block runs too often, performance suffers. Derived stores offer memoized, source‑driven recomputation. This blog contrasts reactive statements and derived stores, shows a counter example, and gives a clear checklist for choosing the right tool.
17 Dec 2025, 01:14 UTC

Problem: Too Many Re‑renders in Big Svelte Apps
In a growing Svelte project, a single $: block that formats a number can end up running dozens of times per second. Why? Because every time any referenced variable changes, the block re‑executes, even if the change has nothing to do with the value you’re formatting. This leads to unnecessary DOM updates, CPU cycles, and a larger bundle when the compiler can’t prune the work.
Thesis: Derived Stores Give You Memoization for Free
Derived stores, created with derived(), recompute only when the source store(s) actually change. They memoize the result and expose a subscription API that Svelte automatically hooks into. For shared or expensive derived data, they keep the component tree lean and the runtime fast.
1. Reactive Statements in Practice
A reactive statement looks like:
let count = 0;
let formatted;
$: formatted = new Intl.NumberFormat().format(count);
Every time count changes, formatted recomputes. But if another variable, say theme, changes, the statement still runs because theme is a referenced variable in the component’s scope. If theme toggles on every click, formatted will be recomputed thousands of times.
Typical debugging: add console.log('formatted updated') inside the block. In a real app you’d use a profiler, but the console suffices to illustrate the problem.
2. Derived Stores: Declarative & Memoized
Derived stores are created with:
import { writable, derived } from 'svelte/store';
const count = writable(0);
const formatted = derived(count, ($count) => {
console.log('formatted recomputed');
return new Intl.NumberFormat().format($count);
});
The derived store subscribes to count and only runs the callback when count changes. The callback receives the current value of count as $count. Inside a component you simply use {$formatted} and Svelte handles the subscription automatically.
Because the derived store memoizes the result, if count is set to the same value again, the callback does not run.
Subscription Pitfall
When you manually subscribe to a derived store (outside of a component), you must unsubscribe() when the consumer is destroyed to avoid memory leaks. Inside a Svelte component, the framework manages this for you.
3. Worked Example: Counter with Format
Both approaches are shown side‑by‑side. The counter increments, and a button toggles an unrelated theme variable. Observe the console logs to see when each derived value updates.
Reactive Version
<script>
let count = 0;
let theme = 'light';
let formatted;
$: {
console.log('reactive formatted recomputed');
formatted = new Intl.NumberFormat().format(count);
}
function inc() { count += 1; }
function toggleTheme() { theme = theme === 'light' ? 'dark' : 'light'; }
</script>
<button on:click={inc}>Increment</button>
<button on:click={toggleTheme}>Toggle Theme</button>
<p>Count: {count}</p>
<p>Formatted: {formatted}</p>
Derived Store Version
<script>
import { writable, derived } from 'svelte/store';
const count = writable(0);
const theme = writable('light');
const formatted = derived(count, ($count) => {
console.log('derived formatted recomputed');
return new Intl.NumberFormat().format($count);
});
function inc() { count.update(n => n + 1); }
function toggleTheme() { theme.update(t => t === 'light' ? 'dark' : 'light'); }
</script>
<button on:click={inc}>Increment</button>
<button on:click={toggleTheme}>Toggle Theme</button>
<p>Count: {$count}</p>
<p>Formatted: {$formatted}</p>
Run the component. Clicking Toggle Theme will trigger the reactive block again, logging a recompute, but the derived store will stay silent. Clicking Increment triggers both, but only the derived store logs when count changes.
4. Trade‑off: Simplicity vs. Performance
- Reactive statements are quick to write, great for local, one‑off calculations that depend on multiple component variables.
- Derived stores add a bit of boilerplate but shine when the derived value is used in many components or when you want to avoid recomputations caused by unrelated state.
- In a small component with a single derived value, the overhead of a store is negligible. In a large app where the same formatted value is needed across many components, a derived store prevents duplicated work.
- Remember: mutating the underlying object without updating the store (e.g.,
count.set({...count, value: 5})whencountis a plain number) will not trigger recomputation. In such cases, usecount.setorcount.updateto ensure the store emits.
5. Actionable Checklist
- Identify values that are derived from stores and shared across components.
- Wrap those values in
derived()and use the store syntax ({$store}) in components. - For component‑local calculations that involve only a few variables, keep a
$:block. - Use
console.logor a profiling tool to verify that derived stores only recompute on source changes. - When manually subscribing to derived stores, always unsubscribe in
onDestroyto avoid leaks.
By following this pattern, you keep your Svelte app responsive, your bundle lean, and your code maintainable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.