Concepts — A Vue-like Renderer, and the Architecture Comparison

Phase 5 · Spec areas §30, §47. Prerequisites: fw-02, fw-03, fw-04.


1. Why this module

fw-02 built diffing. fw-03 built dependency tracking. This module joins them — a VNode renderer driven by reactive effects — and that join is where the real question lives:

What work can each architecture avoid, and how does it know?

A component whose render is wrapped in an effect re-renders only when a value it actually read changes. The diff then only has to cover that component's output, not the tree below it. That is a fundamentally different cost curve from "re-render the subtree and diff," and articulating the difference precisely is the deliverable.


2. Build order

  1. VNode representation; h().
  2. render(vnode, container) — mount.
  3. patch(oldVNode, newVNode) — props, attributes, events, children.
  4. Keyed children diff. Implement a naive version, then the two-ended / longest-increasing- subsequence approach. Measure both on a shuffle.
  5. Components with lifecycle.
  6. The join: wrap each component's render in a fw-03 effect so reactive reads trigger re-render of that component only.
  7. Scheduler: dedupe and flush component updates on a microtask.

3. Failure Lab

  1. Naive keyed diff on a reversal — measure the DOM operation count against the smarter algorithm. Explain the algorithmic difference, not just the number.
  2. Component effect that reads a value conditionally — show the dependency set changing between renders, and what breaks without cleanup (fw-03).
  3. No scheduler: cause the same component to render three times in one tick.
  4. Event handler identity: re-creating handlers each render. Measure the cost of naive removal and re-addition versus a stable indirection.

3.5 Deep dive: keyed diff algorithms, actually compared

Your naive keyed diff and the production one differ algorithmically, and the difference is worth naming rather than accepting.

ApproachReorder costNotes
Naive: for each new child, find it in old, move itO(n²) lookups, O(n) DOM movescorrect, and quadratic
Map-based: index old children by keyO(n) lookups, still O(n) movesmost hand-written diffs stop here
Two-ended: walk from both ends inwardO(n), few moves for common editshandles prepend/append/reverse cheaply
Longest increasing subsequenceO(n log n), minimum movesVue 3's approach for the unmatched middle

The insight behind LIS: after matching keys, the new order is a permutation of the old. Elements already in relative order do not need moving. The longest increasing subsequence of old-indices is the largest set you can leave alone — everything else moves.

For [A,B,C,D,E] → [A,C,B,D,E], a map-based diff may move up to four nodes; LIS moves one.

When does this matter? Rarely, and that is the honest answer — most lists are appended to, not shuffled. It matters for drag-and-drop reordering, sortable tables, and animated list transitions, where every move is also a layout and paint cost. Measure the DOM-operation count in your lab before deciding it is worth the complexity in a system you own.


3.6 Deep dive: the component-effect join, and what it buys

Wrapping each component's render in a reactive effect is a small change with a large consequence:

effect(() => { patch(prevVNode, render(component)); });

Now a reactive read inside that component subscribes that component's render to that value. A change re-renders only that component — its parent does not re-run, and its children do not re-run unless their own dependencies changed.

Compare React: a state change re-renders the component and its subtree, unless memoisation intervenes. The difference is not "Vue is faster" — it is where the default sits:

DefaultEscape hatch
Reactre-render subtreememo, useMemo, useCallback
Vue / signalsre-render nothing but the tracked effectrarely needed

React's default is conservative and predictable; you opt into skipping work, and skipping wrongly causes stale UI. Vue's default is precise; you opt out of tracking, and losing tracking causes stale UI. Both have a failure mode; they are mirror images.

The organisational consequence, which is the part that matters at Principal level: React's model pushes performance work onto every engineer (correct memoisation is a per-component decision), while Vue's pushes it into the framework. That is a real hiring and code-review consideration, and it is a far better basis for a framework decision than benchmark numbers.


3.7 Deep dive: what patch flags add on top

fw-06's compiler annotates each VNode with which parts can change. The runtime then skips comparisons entirely:

// compiled output, conceptually
createElementVNode("div", { class: cls }, text, PatchFlags.CLASS | PatchFlags.TEXT)

patch() reads the flag and updates only class and text — no property enumeration, no full prop diff, no children reconciliation for static subtrees.

This is compile-time knowledge substituting for runtime work, and it is the axis on which the §47 matrix has a column. A template compiler can do this because templates are statically analysable; JSX largely cannot, because it is arbitrary JavaScript.

That single fact — templates are analysable, JSX is not — is the root of most React/Vue architectural divergence. It is not syntax preference, and framing it as such is the mark of someone who has not looked underneath.


4. The comparison matrix (§47)

Fill this from your own implementations. Axes: change detection · scheduling · memory · consistency · debuggability · incremental work · failure modes · extensibility · compile-time knowledge · runtime knowledge.

Rows: mini-react (fw-02/fw-04) · React · mini-vue (fw-03/fw-05) · Vue · signals · Redux (fw-01) · browser style invalidation (bi-07).

Include bi-07. Blink's invalidation sets are a change-detection system built to the same requirement, in C++, at a different scale. Putting it in the same table is the point of running both tracks — and it is the single most valuable row.

The specification is explicit: never reduce this to "which is faster."


5. Principal Engineer Review

  1. A component-level reactive renderer avoids re-rendering children. Name exactly what information it has that React does not, and what it pays for that information.

  2. React chose one model, Vue another, and both ship at enormous scale. What does that tell you about how much these choices actually matter, relative to what else you could work on?

  3. Keyed diff algorithms are a solved problem with real differences. When does the algorithm choice show up in a product, and when is it noise?

  4. Blink's style invalidation and Vue's reactivity solve the same abstract problem. Name two things Blink must handle that Vue does not, and what that costs it.

  5. You are designing a framework for a team of 200. Which model, and what is the deciding factor — performance, debuggability, or hiring?