Vue re-renders components by combining (1) reactive state (Proxy-based in Vue 3), (2) dependency tracking (track/trigger), and (3) a scheduler that batches component updates, then patches the DOM via virtual DOM diffing (optimized with compiler hints like patch flags).
How does Vue track state changes and trigger re-renders internally?
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Core idea
Vue doesn’t “poll” your state. It records who read what during render, and when that state changes it re-runs only the affected render effects.
Step | What happens internally | Why it matters |
|---|---|---|
1) Make state reactive | Vue wraps objects with a Proxy (Vue 3) so it can intercept | Lets Vue observe reads/writes without you calling setState. |
2) Render inside an effect | Each component render runs inside a reactive “effect” (think: tracked function). | Any reactive reads during render become dependencies. |
3) Track dependencies on reads | On | Vue learns exactly which component depends on which property. |
4) Trigger on writes | On | Only affected components/computed/watchers are scheduled. |
5) Batch updates | Component effects are queued (deduped) and flushed in a microtask (scheduler). | Multiple synchronous mutations cause only one re-render per component. |
6) Re-render + patch DOM | Re-run render → new VNode tree → | Virtual DOM diff updates only what changed. |
Dependency tracking (track/trigger) in simplified pseudo-code
// Highly simplified mental model (not Vue source)
const targetMap = new WeakMap();
let activeEffect = null;
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
let dep = depsMap.get(key);
if (!dep) depsMap.set(key, (dep = new Set()));
dep.add(activeEffect);
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
const dep = depsMap?.get(key);
if (!dep) return;
dep.forEach(effect => effect.scheduler ? effect.scheduler(effect) : effect.run());
}
function effect(fn, scheduler) {
const e = {
run() {
activeEffect = e;
try { return fn(); } finally { activeEffect = null; }
},
scheduler
};
e.run();
return e;
}
How “re-render” is actually wired
Each component has a render effect. When its dependencies trigger, Vue doesn’t immediately run it; it queues it.
// Conceptually
const componentUpdateEffect = effect(
() => {
const nextVNodeTree = renderComponent();
patch(prevVNodeTree, nextVNodeTree);
prevVNodeTree = nextVNodeTree;
},
(job) => queueJob(job) // scheduler batching
);
Scheduler / batching
Vue queues component jobs and flushes them in a microtask. So if you do state.a++ then state.b++ synchronously, the component typically renders once. nextTick() resolves after the queued DOM patches are applied.
Vue 3 render performance extra
Vue 3’s compiler adds hints (patch flags + block tree) so the runtime can skip diffing stable parts of the template and focus on dynamic bindings. Reactivity decides which components update; patch flags help decide how much work the DOM patch does within that update.
Reactive primitive | Internal behavior | Update timing |
|---|---|---|
computed() | Runs as a lazy effect; caches value until a dependency triggers (marks it “dirty”). | Recomputes on next access, not immediately on every change. |
watch() | Creates an effect on a getter; on trigger runs a callback (side effects). | Flush can be |
ref() | Wraps a value; reads/writes track/trigger via | Same scheduler rules when used by components. |
Practical implications / common gotchas
• If you destructure reactive objects (e.g., const { x } = reactiveObj), you may lose tracking because the render no longer reads through the Proxy.
• If you mutate many things in a row, expect one render (batched), and use await nextTick() when you need DOM to be updated.
• If something doesn’t update, it usually means the render didn’t read that reactive source (no dependency tracked).
Vue tracks reactive reads during render (track), schedules dependent component effects on writes (trigger → queue), then re-renders and patches the DOM in a batched flush. That’s the whole “state change → re-render” loop.