$derived() Memoization vs $effect() Microtask Scheduling
- Last updated
- Prerequisites:
- Svelte 5 runes basics ($state)
- Understanding of reactive values vs side effects
- svelte
- runes
- derived
- effect
- microtask
- reactivity
Read at your depth
The practical view
$derived computes a value from reactive state: let double = $derived(count * 2);. It recomputes lazily when count changes and is read. $effect runs code after state changes: $effect(() => { console.log(count); });. It runs once initially, then re-runs whenever its read state changes. Use $derived for derived values (it's cached), $effect for side effects (DOM, subscriptions, logging). In Svelte 5 runes you replace Svelte 4's reactive statements ($: double = count * 2) with $derived, and auto-subscriptions with explicit $effect.
The same idea in other frameworks
useMemo ≈ $derived (cached); useEffect ≈ $effect, though React effects run after commit per render.computed() ≈ $derived; effect() in Angular 16+ ≈ $effect.computed() ≈ $derived; watchEffect ≈ $effect (eager, microtask-flushed).Legacy vs modern
Svelte 4 $: reactive statement vs Svelte 5 $derived/$effect
The old $: syntax mixed derived values and side effects ambiguously; runes split them into explicit, memoized $derived and effectful $effect.
$: double = count * 2; // ❌ OLD: reactive statement for a derived value
$: console.log('count', count); // ❌ OLD: side effect mixed into the same syntaxlet double = $derived(count * 2); // ✅ NEW: memoized derived value
$effect(() => console.log('count', count)); // ✅ NEW: explicit side effectInterview gotchas
Context
This tests whether you understand the caching semantics of derived state — interviewers want to know why derived values don't recompute on every render, unlike plain template expressions.
The mechanical answer
$derived creates a computation node that caches its result. It recomputes only when (a) at least one of the signals it read during its last evaluation changed, and (b) something actually reads the derived value. This is lazy memoization with invalidation. A plain expression in the template (e.g. {count * 2}) is re-evaluated when its surrounding template binding re-runs, which may be more often and without caching. The memoization matters when the derived computation is expensive (filtering a large array, formatting): multiple reads of the same $derived in one update cycle share one cached result, whereas repeating the expression recomputes each time. The equality check (===) also suppresses downstream updates when the derived value didn't actually change.
Trap
The naive answer is '$derived recomputes whenever state changes'. It recomputes lazily on read, and only if a dependency changed. Another trap: assuming a $derived with no readers ever runs — it doesn't (lazy), so side effects placed inside a $derived body may never fire. Candidates who explain the lazy read-triggered recomputation and equality suppression show real understanding.
Context
This probes the scheduling and lifecycle — interviewers want the microtask flush timing and the cleanup-before-rerun contract that distinguishes Svelte 5 effects from naive subscriptions.
The mechanical answer
Effects are scheduled on the microtask queue: after a synchronous block mutates state, the runtime flushes the effect queue asynchronously (in a microtask), running each effect in dependency order. This means multiple state changes in one synchronous handler produce ONE effect run, and the effect sees the final values. The initial run happens during component initialization (after the DOM is created, before paint). Before each re-run, the cleanup function (whatever the previous callback returned) executes, letting you tear down subscriptions or DOM listeners. Effects read their dependencies during the run; reads outside the effect (e.g. in an async callback inside it) are not tracked. Writing a signal that the effect also reads triggers another flush — avoid such cycles.
Trap
The naive answer is 'effects run immediately when state changes'. They are batched and deferred to a microtask flush. Another trap: forgetting the cleanup return value — without it, re-running effects leak subscriptions/listeners. Also, writing to state inside an effect without reading it is allowed, but reading-and-writing the same signal causes an infinite flush loop. Candidates who describe the microtask scheduling and the cleanup contract show real depth.