Frontend reactivity often splits into three paths: Signals, Proxy-based reactivity, and the Virtual DOM.
People usually ask:
- Which one is faster?
- Which one is easier to maintain?
- Which one scales better?
If you follow the slogan UI = f(state) all the way back to its origin, the answer becomes surprisingly clear.
This article turns that slogan into a short story, breaks down the core differences between the three reactivity models, shows minimal code comparisons, and ends with a practical decision table—so you can reason about trade-offs and explain them to your team.
The Story Behind “UI = f(state)”
1997 — Functional Reactive Animation
At ICFP 1997, Conal Elliott introduced Functional Reactive Animation, proposing that a UI is fundamentally a pure function:
UI = Time → Graphic
The idea only made small academic waves, but it planted the seed that interfaces can be described as mathematical functions.
2011 — Elm 0.1 made it practical
Ten years later, Evan Czaplicki brought this idea into browsers via Elm 0.1:
view : Model -> Html msg
Model = state.
view = a function that returns UI.
For the first time, frontend developers saw a real, runnable example of UI = f(state).
2013 — React popularized it
At JSConf US 2013, Jordan Walke introduced React and put the slogan directly on the slides:
UI = f(state)
Just give React the latest state and call render(); the framework will figure out how to update the DOM.
The concept left academia, left Haskell, and became our everyday frontend language.
And that’s why the ecosystem split into 3 paths
After React, three different optimization branches emerged:
- Signals — break f into fine-grained pieces 2.Proxy-based reactivity — automate dependency tracking using language features
- Virtual DOM — use diff as the dependency boundary
Understanding this evolution explains why these systems look so different, yet all try to make the same f(state) easier and faster.
Core Question: How do they track, schedule, and update?
For each reactivity model, we’ll compare:
- How are dependencies tracked? (read-time / write-time, granularity)
- How are updates triggered & scheduled? (push/pull, batching, slicing)
- How does DOM get updated minimally? (node-level, attribute-level, tree diff)
Signals — “should this recompute?” lives on the value
1. Dependency tracking
- Dependencies are recorded when you read a signal.
- When you write, only computations that actually used the value rerun.
- Granularity can be as fine as a single primitive value.
2. Scheduling
- Mostly push-based.
- Common patterns: batch(), microtask queues, lazy memoization (hybrid push/pull).
3. DOM updates
- Computations directly update their target DOM node or attribute.
- Almost no tree diffing.
- Extremely efficient for frequent, tiny updates.
Best for
- Sliders
- Maps and markers
- Canvas / charts
- Massive tables with cell-level updates
Proxy Reactivity — automate dependency tracking using language features
1. Dependency tracking
- Uses Proxy(get/set) interception.
- Track dependencies automatically for each accessed property.
- Deep objects register dependencies along the getter chain.
2. Scheduling
- Push-based.
- Writes enqueue jobs in a microtask queue (Vue’s job queue, for example).
3. DOM updates
- Compiler/runtime determines which patch to apply.
- Usually precise, but deep objects incur read-time overhead.
Best for
- Complex forms
- Deeply nested JSON
- Situations where "just mutate objects" is preferred
Virtual DOM — trade tracking for a diff
1. Dependency tracking
- Almost none.
- Rerender the component and diff the new tree vs old tree.
2. Scheduling
-
setState→ component rerenders. - Fiber Scheduler handles priority + time slicing → smoother interactions.
3. DOM updates
- Diff produces a patch list.
- Even small changes require recomputing the component tree first.
Best for
- Large React ecosystems
- Strong SSR/RSC needs
- Teams that rely on DX, tools, and existing patterns more than raw perf
Side-by-side examples
Solid (Signals)
const [count, setCount] = createSignal(0)
createEffect(() => {
console.log('count changed ->', count())
})
setCount(1) // only effects using count re-run
Vue (Proxy Reactive)
const state = reactive({ count: 0 })
watchEffect(() => {
console.log('count changed ->', state.count)
})
state.count++ // dependencies registered along getter chain
React (Virtual DOM)
function Counter() {
const [count, setCount] = useState(0)
console.log('Counter render')
return (
<button onClick={() => setCount(c => c + 1)}>
{count}
</button>
)
}
In Solid/Vue, only the computation using that field reruns.
In React, the entire function reruns → then diff decides the real DOM change.
The real difference isn’t JSX—it’s who decides whether something should recompute:
- Signals / Proxy: data decides (push)
- VDOM: the diff decides (pull)
When should you choose which?
| Scenario | Recommended | Why |
|---|---|---|
| High-frequency interactions (cursor, canvas, charts) | Signals | micro-updates with minimal overhead |
| Huge forms / deep JSON editors | Proxy Reactive | mutate objects directly; dependency tracking auto |
| Team deeply invested in React ecosystem | Virtual DOM | DX, ecosystem, RSC/SSR, talent availability |
| Hybrid usage: mostly React but hotspots need speed | React + Signals | fine-grained reactivity only where needed |
Summary
Signal
Encodes where to update inside the value itself.
Writes notify exactly the consumers that depend on it.
Proxy Reactive
Uses ES6 Proxy to turn any property into a trackable value automatically.
Virtual DOM
Ignores dependency tracking and relies on diffing snapshots of f(state).
If you understand how each model distributes cost across:
- dependency tracking
- scheduling
- DOM updates
you can choose the right reactivity strategy for any project.
Next Up
The next article in this series will dive deep into how Signals actually work, from dependency graphs to schedulers, and how to implement your own minimal version.

Top comments (0)