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
- VNode representation;
h(). render(vnode, container)— mount.patch(oldVNode, newVNode)— props, attributes, events, children.- Keyed children diff. Implement a naive version, then the two-ended / longest-increasing- subsequence approach. Measure both on a shuffle.
- Components with lifecycle.
- The join: wrap each component's render in a
fw-03effect so reactive reads trigger re-render of that component only. - Scheduler: dedupe and flush component updates on a microtask.
3. Failure Lab
- Naive keyed diff on a reversal — measure the DOM operation count against the smarter algorithm. Explain the algorithmic difference, not just the number.
- Component effect that reads a value conditionally — show the dependency set changing between
renders, and what breaks without cleanup (
fw-03). - No scheduler: cause the same component to render three times in one tick.
- 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.
| Approach | Reorder cost | Notes |
|---|---|---|
| Naive: for each new child, find it in old, move it | O(n²) lookups, O(n) DOM moves | correct, and quadratic |
| Map-based: index old children by key | O(n) lookups, still O(n) moves | most hand-written diffs stop here |
| Two-ended: walk from both ends inward | O(n), few moves for common edits | handles prepend/append/reverse cheaply |
| Longest increasing subsequence | O(n log n), minimum moves | Vue 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:
| Default | Escape hatch | |
|---|---|---|
| React | re-render subtree | memo, useMemo, useCallback |
| Vue / signals | re-render nothing but the tracked effect | rarely 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
-
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.
-
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?
-
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?
-
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.
-
You are designing a framework for a team of 200. Which model, and what is the deciding factor — performance, debuggability, or hiring?