Quick answer
React invalidates a component and then diffs the result. Vue tracks which state each DOM node read and updates only that node. Same goal, different invalidation granularity — and that difference explains the performance profile, the mental model, and the debugging workflow.
Most React vs Vue arguments stall on syntax: JSX or templates, hooks or the Composition API. Those are surface differences. Under the hood both frameworks answer one question — how do you know what changed? — and they answer it in opposite ways.
React: re-render, then reconcile
React treats the UI as a function of state. When setState runs, React schedules the component function to run again. That call returns a fresh element tree. React compares it with the previous tree — same type means update props in place, different type means tear down and remount — then commits only the DOM mutations that survived the diff.
The important part is what React does not know. It does not know that a single <span> reads count. It knows a component’s state changed, so the whole function runs again and the diff sorts out the rest.
function Counter() {
const [count, setCount] = useState(0);
console.log("render"); // runs on every update
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
That is why memoization exists. React.memo, useMemo, and useCallback narrow an invalidation boundary the model leaves wide open. React 18’s concurrent renderer then decides when to do the work, which is what makes transitions and time-slicing possible.
Vue: track, then update the exact node
Vue wraps state in a Proxy. During a render, every property read is recorded against the currently running effect. When a property is written, Vue looks up the dependency set for that key and schedules exactly those effects. No component re-execution. No tree comparison.
const state = reactive({ count: 0 });
effect(() => console.log(state.count)); // tracks count
state.count++; // re-runs only effects that read count
The scheduler queues jobs in a microtask and deduplicates them, so ten synchronous writes still produce one flush. computed values cache until a dependency marks them dirty.
On top of that, Vue’s template compiler works at build time: static subtrees are hoisted out of the render function, dynamic bindings get patch flags, and the block tree skips unchanged regions. A Vue component can update one text node while the rest of the template is never touched.
The compiler is the newer fault line
Both frameworks have moved toward compile-time help from opposite directions. Vue’s compiler has always annotated templates; React historically did everything at runtime. The React Compiler now inserts memoization automatically, shrinking React’s invalidation boundary toward Vue’s without changing the re-render model underneath.
Where each model strains
React’s cost shows up as re-render pressure. Deep trees, context updates, and careless object props force work that ends in a no-op diff. The familiar failure modes: stale closures, dependency arrays, and memo sprinkled everywhere as a band-aid.
Vue’s cost shows up at the edges of reactivity. Destructuring a reactive object loses the proxy. Passing values through non-reactive boundaries silently stops updates. Very large objects pay a proxy cost on every access. Vue gives you a precise system, but you have to stay inside it.
What this means in practice
React is easier to reason about at the top: state changes, UI is a pure projection, the diff handles the rest. That predictability scales to large teams and unusual architectures, including React Native.
Vue is easier to reason about at the bottom: this value changed, this node updates. For form-heavy interfaces, dashboards, and apps with constant small state changes, that granularity removes a whole category of manual optimization.
Neither is faster in the abstract. React does more work per update but can interrupt and prioritize it. Vue does less work per update but tracks dependencies eagerly. The useful question is not which is faster — it is which cost you would rather manage.
Common pitfalls
- Assuming React re-renders the DOM. It re-renders the component; the diff decides what reaches the DOM.
- Destructuring reactive state in Vue and wondering why the view stopped updating.
- Reaching for
memobefore measuring where the re-render starts. - Treating Vue’s compiler optimizations as permission to ignore component size.
Pro tip
When either app feels slow, log the update boundary instead of the frame rate. In React, count component renders per interaction. In Vue, log which effects fire per state change. The framework’s model tells you where to look.
Once invalidation granularity is the lens, the syntax debate stops mattering. Pick the invalidation model your team can debug at 2 a.m.



0 Comments