Reactivity
Date: 2026-08-19
The one question every framework has to answer: when a value changes, how do we know what to update? There are three families of answer, they differ mainly in when the dependency is worked out, and that timing decides almost everything else about how the framework feels.
Reactivity is the mechanism connecting a change in state to the specific DOM updates it requires.
Three families. The difference is when the framework learns what depends on what:
WHEN IS THE DEPENDENCY KNOWN?
RE-RUN + DIFF never — recompute everything, compare, patch the difference
React
unit of work: the component
RUNTIME TRACKING at read time — a getter records who was reading it
Vue, Solid, Angular signals
unit of work: the expression
COMPILE-TIME at build time — the compiler reads the source and emits
the update statements directly
Svelte
unit of work: the statement
Re-run and diff
State changes, the component function runs again from the top, produces a new description, and the framework compares it with the previous one to find the real DOM changes. See Virtual DOM.
The trade is honesty for work. Nothing has to be declared: any value read during render is a dependency by construction, so it can’t go stale. The cost is that the framework re-runs code that couldn’t possibly have changed, and the developer takes on a second job — telling it what to skip, through memoisation and stable references.
Characteristic failure: the stale closure. A function created in one render captures that render’s values; called later, it reports the past. Every “why is my count one behind” question is this.
Runtime dependency tracking
Reading a reactive value goes through a getter. If something is currently being computed, the getter records the pair. Writing notifies exactly those subscribers. See Signals.
The trade is bookkeeping for precision. Updates reach the single text node that changed without re-running the component, so cost scales with what changed rather than with tree size. The cost is that the tracking only works inside a reactive context — read a value outside one and you get a plain snapshot with no subscription, silently.
Characteristic failure: losing reactivity by destructuring. Pulling .value out into a local variable copies the number and drops the getter, so nothing ever updates again — and nothing errors.
Compile-time reactivity
The compiler analyses the source, builds the dependency graph statically, and emits imperative update code. No diffing algorithm ships to the browser and no dependency-tracking runtime does either.
The trade is flexibility for size. It’s the smallest and fastest of the three at runtime, and it needs the compiler to be able to see the assignment. Dynamism the compiler can’t follow has to be routed through explicit reactive primitives.
What this decides for you
| Re-run + diff | Runtime tracking | Compiled | |
|---|---|---|---|
| Update granularity | Component subtree | Expression | Statement |
| Cost scales with | Tree size | Number of subscribers | Nothing at runtime |
| Manual optimisation | Memoisation, stable refs | Rare | Rare |
| Runtime shipped | Framework + diff | Framework + tracking | Almost none |
| Fails by | Re-rendering too much | Losing the subscription | Compiler can’t see it |
Convergence is real, in one direction. React shipped a compiler that inserts memoisation for you; Angular made signals its reactivity model; Vue — signal-shaped from the start — added a compiled mode that skips diffing entirely; Svelte rebuilt its compiled reactivity on signals underneath. The industry decided fine-grained tracking was right — the surviving disagreement is whether you write it or a compiler infers it.
The thing worth carrying
Granularity is the whole story. Ask of any framework: when this one number changes, what is the smallest thing that runs again? A component, an expression, or an assignment. That single answer predicts its performance profile, its debugging experience, and which optimisations you’ll be asked to do by hand.