Concepts — A Vue-like Renderer, and the Architecture Comparison
Phase 5 · Spec areas §30, §47. Prerequisites: fw-02, fw-03, fw-04.
0. Orientation
The 60-second version
fw-02 built diffing. fw-03 built dependency tracking. This module joins them, and the join
is where the real question lives:
What work can each architecture avoid, and how does it know?
The join itself is three lines:
effect(() => { patch(prevVNode, render(component)); });
Now a reactive read inside a 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. That is a fundamentally different cost curve from "re-render the subtree and diff," and articulating the difference precisely is the deliverable.
The comparison that actually matters
| Default | Escape hatch | Failure mode | |
|---|---|---|---|
| React | re-render subtree | memo, useMemo, useCallback | memoise wrongly → stale UI |
| Vue / signals | re-render nothing but the tracked effect | rarely needed | lose tracking → stale UI |
They are mirror images, and neither is free. But the consequence that matters at Principal level is organisational, not technical:
React's model pushes performance work onto every engineer — correct memoisation is a per-component decision made hundreds of times. Vue's pushes it into the framework. That is a hiring and code-review consideration, and it is a far better basis for a framework decision than a benchmark.
The second thread: keyed diff algorithms
Your naive keyed diff and the production one differ algorithmically, and the module asks you to measure the gap rather than accept it. The honest conclusion is that it rarely matters — most lists are appended to, not shuffled — and knowing exactly when it does (drag-and-drop, sortable tables, animated reordering, where every move is also layout and paint) is the useful part.
What you should be able to do after this module
| Capability | Evidence you have it |
|---|---|
| Compare architectures precisely | Say what information each has, and what it paid to get it |
| Choose for an organisation | Argue from where performance work lands, not from benchmarks |
| Price a diff algorithm | Measure DOM-operation counts before adopting LIS complexity |
| Explain compile-time leverage | Say why templates are analysable and JSX largely is not |
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."
Core terminology
| Term | Definition |
|---|---|
| VNode | A plain-object description of an element; the unit both render and patch operate on |
h() | The VNode factory; the runtime equivalent of what a template compiles to |
| Mount | First render — create real DOM from a VNode tree |
| Patch | Update — diff old against new VNode and apply the minimum |
| Keyed diff | Child reconciliation using author-supplied identity rather than position |
| Two-ended diff | Walking from both ends inward; cheap for prepend, append, and reversal |
| LIS | Longest increasing subsequence — the largest set of children needing no move |
| Patch flag | Compiler annotation of which parts of a node can change (fw-06) |
| Static hoisting | Creating a fully-static subtree once instead of per render |
| Component effect | The fw-03 effect wrapping a component's render — the join this module makes |
| Job queue / scheduler | Deduplicated, microtask-flushed queue of pending component updates |
| Pre / post flush | Ordering of jobs relative to DOM update; post exists so callbacks can measure |
| Handler caching | Stable indirection so re-created handlers do not cause remove/add churn |
Mental models
"Which architecture is faster" is the wrong question; "what does each know" is the right one. A reactive renderer avoids re-rendering children because it tracked which values each component read. It paid for that information with per-read bookkeeping and an invisible dependency graph. React avoids that bookkeeping and pays by doing work proportional to the rendered tree. Both trades are defensible; neither is free.
Defaults determine where the work lands, and defaults are organisational. A framework whose default is conservative pushes optimisation decisions to every engineer on every component. A framework whose default is precise absorbs them. At 200 engineers that difference dominates any benchmark, and it is the honest basis for the decision.
Compile-time knowledge substitutes for runtime work. Patch flags let patch() skip property
enumeration and children reconciliation entirely. 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.
Algorithmic wins need a workload before they need an implementation. LIS gives minimum moves in
O(n log n). For [A,B,C,D,E] → [A,C,B,D,E] a map-based diff may move four nodes; LIS moves one.
And most production lists are appended to, so measure your DOM-operation count on your real
workload before importing the complexity into a system you own.
Common misconceptions
-
"Vue is faster than React." Vue's default avoids more work. React's escape hatches reach the same place when applied correctly — and "when applied correctly" is doing enormous work in that sentence, which is exactly the organisational point.
-
"Fine-grained updates have no cost." Every reactive read does bookkeeping, and the dependency graph scales with reads. At high read counts and small trees, re-run-and-diff wins.
-
"A better keyed diff algorithm will speed up our lists." Only if your lists reorder. Measure DOM operations on a real workload; append-heavy lists see nothing from LIS.
-
"Patch flags are just an optimisation." A wrong patch flag is a silent correctness bug — a dynamic node marked static simply never updates, with no error. That is
fw-06's central risk and it belongs in the review of any compiler change. -
"JSX and templates are a syntax preference." They differ in what a compiler can prove. Everything downstream — hoisting, patch flags, dependency analysis — follows from that.
-
"You don't need a scheduler if updates are fine-grained." Without dedup and microtask flushing, one component renders three times in a tick. Precision does not imply batching.
-
"Re-creating event handlers each render is fine." It is, if you use a stable indirection. Naive removal and re-addition per render is measurable churn — your failure lab quantifies it.
Interview talking points
- "Joining reactivity to a renderer is three lines — wrap the component's render in an effect — and it changes the cost curve completely. A reactive read inside the component subscribes that component's render to that value, so a change re-renders that component and nothing above or below it."
- "React and Vue are mirror images, not better and worse. React's default is conservative: you opt into skipping work, and skipping wrongly gives you stale UI. Vue's default is precise: you opt out of tracking, and losing tracking gives you stale UI. Same failure, opposite direction."
- "The decision I'd actually make for a 200-engineer org is about where performance work lands. React pushes correct memoisation onto every engineer on every component; Vue absorbs it into the framework. That's a hiring and code-review consideration, and it's a much better basis than a benchmark."
- "Templates are statically analysable and JSX largely isn't. That one asymmetry is the root of most React/Vue architectural divergence — hoisting, patch flags, dependency analysis all follow from it. Framing it as a syntax preference is a tell that someone hasn't looked underneath."
- "Keyed diff algorithms are a solved problem with real differences and a narrow application. LIS gives minimum moves, but most lists are appended to. It matters for drag-and-drop, sortable tables and animated reordering, where every move is also a layout and paint — so I'd want the DOM-operation count from the real workload before taking on the complexity."
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?
Connections to other modules
fw-02— the diffing half of the join, and where keyed reconciliation was first built.fw-03— the tracking half. The component effect is that module'seffect()applied at component granularity.fw-04— the third strategy for the same pressure: make a big render interruptible rather than making renders small.fw-06— patch flags and static hoisting are generated there; this module'spatch()is what consumes them.fw-11— read Vue'sruntime-corescheduler andruntime-domprop patching against yours, and find the LIS computation.bi-07— Blink's invalidation sets solve the same abstract problem in C++ at a different scale. It is the single most valuable row in your §47 table.fe-01— why the job queue flushes on a microtask, and why that is a batching boundary rather than a yield.