๐Ÿณ IntuitiveFE
Login
โ† All concepts

Virtual DOM vs. Signals

โฑ๏ธ ~3-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

๐Ÿ“ธ React redraws the blueprint. โšก Signals flip one wire.

Virtual DOM (React): you change one light switch, so the architect redraws the whole floor plan for that wing, lays the new plan over the old one, and circles what differs โ€” then only repaints the differences. Smart about painting, but it re-drew the whole wing to find out what changed.

Signals: the switch is wired directly to exactly one bulb. Flip it and only that bulb reacts. Nobody redraws anything. The wiring (subscription) was set up the first time the bulb "read" the switch โ€” so the system already knows the precise blast radius of any change.

The Staff-level line: React's unit of work is the component; Signals' unit of work is the read.

๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.
Change state:
App
Headerreads user
Countermemoowns statere-ran
Displayreads countre-ran
Label (static)re-ran
Footerreads theme

3

React re-runs (count++)

1

Signals re-runs (count++)

// React: owner re-renders + entire subtree re-runs by default (then the VDOM diff decides what to commit).

๐ŸŽฏ

Challenge

Make a `count` change re-run exactly ONE node. Which model gets you there?

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Exact interview question

โ€œA list of 10,000 rows re-renders sluggishly when you edit one cell. Walk me through why, and how React vs. a Signals-based framework would each handle it.โ€

Strong answer: in React the owning component re-renders and its subtree's render functions re-run; the reconciler diffs and commits only changed DOM, but the render work already happened โ€” you fix it with memo, stable references, virtualization, or lifting the cell's state down so the blast radius shrinks. In a Signals model the edited cell's signal has exactly one subscriber, so only that cell's text node updates โ€” no subtree render, no diff.

Production bug it solves

A context value that changes every render (new object literal in the provider) silently re-renders every consumer subtree โ€” the classic "why is my whole app re-rendering" incident. Knowing the unit of work tells you the fix: memoize the context value, split contexts, or move to atom/signal state where only readers of the changed slice react.

๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

React fiber + concurrent rendering:

  • A render is split into render phase (build the work-in-progress fiber tree, can be interrupted/restarted โ€” this is why render must be pure) and commit phase (apply DOM mutations, run layout effects โ€” synchronous, not interruptible).
  • Concurrent features (useTransition, startTransition) let React pause a render to keep the main thread responsive, then resume โ€” built on the interruptible render phase. useDeferredValue renders a stale value at high priority, the fresh value at low.
  • memo/useMemo/useCallback prune render work by reference equality. They reduce the render, not just the commit.

Fine-grained reactivity (Signals / Solid / Angular Signals / Vue ref):

  • A signal is an observable cell. Reading it inside a tracking scope (an effect/computed) registers a subscription; writing it notifies exactly those subscribers. This is dependency tracking at read time โ€” no dependency arrays, no diffing.
  • Solid runs the component function once; JSX compiles to direct DOM creation + signal-bound effects, so there's no VDOM and no component re-render โ€” only the bound expressions re-run.
  • Angular is mid-migration: Zone.js monkey-patches async APIs to know "something might have changed" and runs change detection top-down (mitigated by OnPush + immutable inputs). Signals + zoneless replace that with precise, push-based updates โ€” CD only where a signal read changed.

The trade-off to name out loud: VDOM buys a simpler mental model (re-render top-down, let the framework diff) and great ecosystem/SSR maturity, at the cost of render work proportional to tree size. Signals buy work proportional to what changed, at the cost of a sharper mental model (you must understand tracking scopes) and trickier "read outside a reactive context" footguns.

๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

An in-memory representation of the UI used to batch and minimize real DOM mutations.

The component that owns the state re-renders, plus its entire subtree by default โ€” memo boundaries can prune branches.

Signals re-run only the exact expression that read the changed value; React re-runs the owning component's subtree.

React: the owning component re-renders + subtree; fix with memo, stable refs, or virtualisation. Signals: only the one subscribed expression updates.

Render is a pure description; commit applies real DOM mutations that users can see.

Tearing is when two components read the same external store and get different values in one render pass due to an intervening store update.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

What does React.memo do to prevent unnecessary renders?