DEV Community

Cover image for Signals vs Proxy vs Virtual DOM — What Actually Makes Them Different?
Luciano0322
Luciano0322

Posted on

Signals vs Proxy vs Virtual DOM — What Actually Makes Them Different?

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)”

story timeline

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Signals — break f into fine-grained pieces 2.Proxy-based reactivity — automate dependency tracking using language features
  2. 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:

  1. How are dependencies tracked? (read-time / write-time, granularity)
  2. How are updates triggered & scheduled? (push/pull, batching, slicing)
  3. 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
Enter fullscreen mode Exit fullscreen mode

Vue (Proxy Reactive)

const state = reactive({ count: 0 })

watchEffect(() => {
  console.log('count changed ->', state.count)
})

state.count++ // dependencies registered along getter chain
Enter fullscreen mode Exit fullscreen mode

React (Virtual DOM)

function Counter() {
  const [count, setCount] = useState(0)
  console.log('Counter render')

  return (
    <button onClick={() => setCount(c => c + 1)}>
      {count}
    </button>
  )
}
Enter fullscreen mode Exit fullscreen mode

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.


References

Top comments (0)