| |

Vue.js 100 🟢 Vue Core Internals: Deep Dive into Mount Phase, Render Pipeline, and Scheduler Queue

⏱️ Estimated read: 12 min | |

The mount phase, the render pipeline, and the scheduler queue are three stages of the same mechanism. The mount phase creates the component instance, runs the setup, and executes the render function for the first time. The render pipeline turns the template into a virtual DOM tree and patches it into real DOM nodes. The scheduler queue collects the reactive effects that need to re-run and flushes them in a single batch, in the correct order, at the microtask boundary. Each stage has a specific entry point in the runtime, and each is responsible for a distinct part of the component’s lifecycle.

This chapter traces the three stages from the internal functions that implement them: mountComponent, setupRenderEffect, and queueJob/flushJobs. It examines the effect that wraps the render function, the distinction between the initial mount and the update path inside the same effect, and the queue that batches updates and guarantees parent-before-child execution order.

Key point: The render function is not called directly. It is wrapped in a reactive effect, and the effect’s scheduler is queueJob. When a reactive dependency changes, the effect does not re-run immediately; it is pushed into the scheduler queue and flushed as a microtask. The queue is sorted by component UID, which is why parent components update before their children.


Why these internals matter

The effect wrapper problem. A component’s render function is executed inside a ReactiveEffect. The first run of the effect is the mount. The subsequent runs are the updates. The effect is what makes the render reactive: it tracks every reactive property that the render function reads, and when one of them changes, the effect is triggered . Understanding this wrapper explains why a component re-renders when its state changes and why the render function must be pure.

The scheduler batching problem. If a reactive effect re-ran immediately on every trigger, a component that updates three state variables in one handler would render three times. The scheduler queue exists to batch these triggers: the effect is pushed into a queue, and the queue is flushed once in a microtask. The effect runs once, not three times, for the same tick . The nextTick function returns the promise that resolves after the flush, which is why await nextTick() guarantees that the DOM is updated.

The ordering problem. The queue is sorted by the component’s uid before flushing. The uid is assigned incrementally when the component instance is created, and the parent is always created before the child. Sorting by uid guarantees that parent components update before their children, which is necessary because a child’s props may depend on the parent’s new state . Without the sort, a child could render with stale props from a parent that has not yet updated.


a. The Mount Phase: createComponentInstance and setupRenderEffect

The mountComponent function is the entry point for mounting a component. It performs three operations: creating the component instance, setting up the instance, and creating the render effect .

// Conceptual mountComponent flow
const instance = (initialVNode.component = createComponentInstance(
  initialVNode, parentComponent, parentSuspense
))
setupComponent(instance)
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense)

The createComponentInstance function creates an object that holds the component’s state: props, attrs, slots, setupState, subTree, and isMounted. The setupComponent function runs the component’s setup() function and initializes the props and slots. The setupRenderEffect function creates the reactive effect that wraps the render function .

// Conceptual setupRenderEffect
const setupRenderEffect = (instance, initialVNode, container, ...) => {
  instance.update = effect(function componentEffect() {
    if (!instance.isMounted) {
      // MOUNT: render the component's subtree
      const subTree = (instance.subTree = renderComponentRoot(instance))
      // Patch the subtree into the container
      patch(null, subTree, container, anchor, instance, parentSuspense)
      // Store the root DOM node
      initialVNode.el = subTree.el
      instance.isMounted = true
    } else {
      // UPDATE: re-render and patch
      const nextTree = renderComponentRoot(instance)
      const prevTree = instance.subTree
      instance.subTree = nextTree
      patch(prevTree, nextTree, ...)
    }
  }, prodEffectOptions)
}

The componentEffect function is the effect’s runner. On the first run, instance.isMounted is false, and the function executes the mount path: it calls renderComponentRoot to execute the render function and produce the subtree VNode, then calls patch with null as the old VNode to mount the subtree into the container . After the mount, isMounted is set to true, and subsequent runs of the effect take the update path.

The prodEffectOptions object contains the scheduler. In production, the scheduler is queueJob, which means that when a reactive dependency changes, the effect is not run immediately; it is pushed into the scheduler queue.


b. The Render Pipeline: renderComponentRoot and patch

The render function is not the only step in the pipeline. The renderComponentRoot function executes the component’s render function, normalizes the result, and returns the subtree VNode. The patch function then compares the subtree to the previous one and applies the differences to the DOM.

The render function can be the compiled template, a manually written render function, or the function returned by setup(). The renderComponentRoot function calls the appropriate function and handles the result. If the result is an array, it wraps it in a Fragment VNode. If the result is a single VNode, it returns it directly.

The patch function is the recursive diffing function. On the initial mount, patch(null, subTree, ...) is called, which means there is no old VNode to compare against. The function dispatches on the VNode’s type: an element VNode goes to processElement, a component VNode goes to processComponent, a text VNode goes to processText, and so on . Each processor either mounts the node (when n1 is null) or updates it (when n1 and n2 are both present).

The render pipeline is compiler-informed. The template compiler emits patchFlag hints on dynamic VNodes, and the patchElement function reads these flags to update only the properties that can change. A static element with no patchFlag is skipped entirely during the patch phase . This is why the compiled template is faster than a manually written render function that creates new VNodes without the flags.


c. The Scheduler Queue: queueJob and flushJobs

The scheduler queue is the mechanism that batches updates. The queueJob function pushes the component’s update effect into the queue and schedules a flush. The flushJobs function sorts the queue by component uid, runs each job, and resets the state.

// Conceptual scheduler implementation
const queue = []
let isFlushing = false
let isFlushPending = false
let currentFlushPromise = null
const resolvedPromise = Promise.resolve()

function queueJob(job) {
  if (!queue.includes(job)) {
    queue.push(job)
  }
  queueFlush()
}

function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

function flushJobs() {
  isFlushing = true
  isFlushPending = false
  queue.sort((a, b) => a.id - b.id)
  for (const job of queue) {
    job()
  }
  queue.length = 0
  isFlushing = false
  currentFlushPromise = null
}

The queue.includes(job) check prevents the same job from being pushed twice in the same flush. If a component’s state changes three times in one handler, the effect is triggered three times, but queueJob is called with the same job each time, and the includes check ensures that only one copy is in the queue . The effect runs once.

The isFlushPending flag prevents multiple microtasks from being scheduled. The first queueJob call sets the flag and schedules the flush. Subsequent calls in the same tick see the flag and do not schedule another microtask. The flush runs once.

The queue.sort((a, b) => a.id - b.id) is what guarantees parent-before-child execution. The component’s uid is assigned incrementally when createComponentInstance runs, and the parent is always created before its children. A smaller uid means the component was created earlier, which means it is higher in the tree. Sorting by uid ensures that the parent’s update effect runs before the child’s .

The nextTick function returns the currentFlushPromise if a flush is pending, or the resolved promise otherwise. Awaiting it guarantees that the flush has completed and the DOM is updated .


Complete Example Session

// ============================================
// PART 1: MOUNT COMPONENT
// ============================================
// Entry point for mounting a component
// 1. createComponentInstance
// 2. setupComponent (runs setup())
// 3. setupRenderEffect (creates the render effect)


// ============================================
// PART 2: SETUP RENDER EFFECT
// ============================================
// The effect wraps componentEffect
// First run: mount path
// Subsequent runs: update path


// ============================================
// PART 3: MOUNT PATH INSIDE THE EFFECT
// ============================================
// if (!instance.isMounted) {
//   const subTree = renderComponentRoot(instance)
//   patch(null, subTree, container, ...)
//   instance.isMounted = true
// }


// ============================================
// PART 4: UPDATE PATH INSIDE THE EFFECT
// ============================================
// else {
//   const nextTree = renderComponentRoot(instance)
//   const prevTree = instance.subTree
//   patch(prevTree, nextTree, ...)
// }


// ============================================
// PART 5: THE SCHEDULER OPTION
// ============================================
// prodEffectOptions = { scheduler: queueJob }
// When a reactive dependency changes, the effect is not run
// queueJob pushes the effect into the scheduler queue


// ============================================
// PART 6: QUEUE JOB DEDUPLICATION
// ============================================
// queue.includes(job) prevents duplicate entries
// Three state changes in one handler → one effect run


// ============================================
// PART 7: FLUSH JOBS SORTING
// ============================================
// queue.sort((a, b) => a.id - b.id)
// Parent (smaller uid) runs before child (larger uid)


// ============================================
// PART 8: NEXT TICK
// ============================================
// await nextTick() waits for the flush to complete
// Guarantees the DOM is updated

The eight parts trace the mount entry, the effect creation, the two paths inside the effect, the scheduler option, the deduplication, the sorting, and the nextTick guarantee.


Quick Reference

Mount Phase Functions

FunctionPurpose
mountComponentEntry point for mounting
createComponentInstanceCreates the instance object
setupComponentRuns setup(), initializes props/slots
setupRenderEffectCreates the reactive render effect

The Effect Paths

PathConditionAction
Mount!instance.isMountedRender subtree, patch(null, subTree)
Updateinstance.isMountedRender new subtree, patch(prevTree, nextTree)

Scheduler Functions

FunctionPurpose
queueJob(job)Push effect into queue, schedule flush
queueFlush()Schedule the microtask flush
flushJobs()Sort queue, run jobs, reset state
nextTick()Wait for flush to complete

Ordering Guarantee

MechanismEffect
uid assignmentIncremental, parent before child
queue.sort((a, b) => a.id - b.id)Parent updates before child

Best Practices

✅ Do This:

// Use nextTick to wait for DOM updates
await nextTick()
console.log(element.textContent)                                       // ✅
// Keep render functions pure; side effects belong in effects
function render() { return h('div', count.value) }                     // ✅
// Batch state updates in the same handler
function handleClick() {
  count.value++
  flag.value = true
}                                                                       // ✅

❌ Don’t Do This:

// Assume the DOM is updated synchronously after a state change
count.value++
console.log(element.textContent)  // stale                               // ❌
// Create effects after await in nested callbacks
fetch().then(() => watchEffect(() => {}))  // orphaned                  // ❌
// Assume the scheduler runs on a macrotask
setTimeout(() => {}, 0)  // flush happens before this                    // ❌

Common Pitfalls

PitfallWhy It HappensFix
DOM is stale after state changeScheduler flushes in microtaskawait nextTick()
Child renders with stale propsParent not updated firstQueue sorted by uid handles this
Effect runs multiple timesDuplicate jobs in queuequeue.includes deduplication
Infinite update loopEffect triggers itselfcheckRecursiveUpdates warning

Real-World Examples

1. Mount Entry

const instance = createComponentInstance(vnode, parent)
setupComponent(instance)
setupRenderEffect(instance, vnode, container)

2. Effect Creation

instance.update = effect(componentEffect, { scheduler: queueJob })

3. Mount Path

const subTree = renderComponentRoot(instance)
patch(null, subTree, container)
instance.isMounted = true

4. Update Path

const nextTree = renderComponentRoot(instance)
patch(instance.subTree, nextTree)
instance.subTree = nextTree

5. Queue Job

queue.push(job)
queueFlush()

6. Flush Jobs

queue.sort((a, b) => a.id - b.id)
queue.forEach(job => job())

7. Next Tick

await nextTick()

8. UID Assignment

instance.uid = uid++

9. Deduplication

if (!queue.includes(job)) queue.push(job)

10. Parent Before Child

// uid 1 (parent) runs before uid 2 (child)

Visual

The Mount Phase

┌──────────────────────────────────────────────────────────────┐
│  mountComponent                                              │
│         │                                                    │
│         ├── createComponentInstance                          │
│         ├── setupComponent (runs setup())                    │
│         └── setupRenderEffect                                │
│                    │                                         │
│                    ▼                                         │
│         ┌─────────────────────┐                              │
│         │ effect(             │                              │
│         │   componentEffect,  │                              │
│         │   { scheduler:      │                              │
│         │     queueJob }      │                              │
│         │ )                   │                              │
│         └─────────┬───────────┘                              │
│                   │                                          │
│                   ▼                                          │
│         ┌─────────────────────┐                              │
│         │ componentEffect()   │  First run: mount            │
│         │   renderComponentRoot│                              │
│         │   patch(null, subTree)│                             │
│         │   isMounted = true  │                              │
│         └─────────────────────┘                              │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Scheduler Queue

┌──────────────────────────────────────────────────────────────┐
│  STATE CHANGE                                                │
│         │                                                    │
│         ▼                                                  │
│  effect triggered                                            │
│         │                                                    │
│         ▼                                                  │
│  scheduler: queueJob(effect)                                │
│         │                                                    │
│         ▼                                                  │
│  ┌─────────────────────┐                                    │
│  │ queue = [job]       │                                    │
│  │ queueFlush()        │                                    │
│  └─────────┬───────────┘                                    │
│            │                                                │
│            ▼                                                │
│  ┌─────────────────────┐                                    │
│  │ resolvedPromise     │  Microtask                         │
│  │ .then(flushJobs)    │                                    │
│  └─────────┬───────────┘                                    │
│            │                                                │
│            ▼                                                │
│  ┌─────────────────────┐                                    │
│  │ flushJobs()         │                                    │
│  │ sort by uid         │                                    │
│  │ run each job        │                                    │
│  └─────────────────────┘                                    │
│                                                              │
│  nextTick() returns the flush promise.                       │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Parent Before Child

┌──────────────────────────────────────────────────────────────┐
│  QUEUE BEFORE SORT                                           │
│  [childJob(uid=2), parentJob(uid=1)]                         │
│                                                              │
│  queue.sort((a, b) => a.id - b.id)                           │
│                                                              │
│  QUEUE AFTER SORT                                            │
│  [parentJob(uid=1), childJob(uid=2)]                         │
│                                                              │
│  Parent renders first.                                       │
│  Child renders with the parent's new props.                  │
│                                                              │
│  Without the sort, the child would render with stale props.  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Mount entrymountComponent
Instance creationcreateComponentInstance
Render effectsetupRenderEffect → effect(componentEffect)
Mount path!isMounted → renderComponentRoot + patch(null, subTree)
Update pathisMounted → patch(prevTree, nextTree)
Scheduler option{ scheduler: queueJob }
Queue deduplicationqueue.includes(job)
Flush orderingqueue.sort((a, b) => a.id - b.id)
Next tickReturns the flush promise
MicrotaskPromise.resolve().then(flushJobs)

Key takeaways:

  • The render function is wrapped in a reactive effect. The effect is created in setupRenderEffect, and its first run is the mount. Subsequent runs are the updates. The effect tracks every reactive property that the render function reads, and when one changes, the effect is triggered .
  • The mount path and the update path are two branches of the same effect. The componentEffect function checks !instance.isMounted to decide whether to mount or update. The mount path calls patch(null, subTree), and the update path calls patch(prevTree, nextTree).
  • The scheduler batches updates into a single flush. The queueJob function pushes the effect into a queue and schedules a flush. The queue.includes(job) check prevents duplicate entries, so multiple state changes in one tick produce one render .
  • The queue is sorted by component uid. The uid is assigned incrementally when the component instance is created. Sorting by uid guarantees that parent components update before their children, which is necessary for correct prop propagation .
  • nextTick returns the flush promise. Awaiting it guarantees that the scheduler has finished running the queued jobs and the DOM has been updated. This is the mechanism that makes await nextTick() a reliable way to read the DOM after a state change .

Remember: The mount phase, the render pipeline, and the scheduler queue are three views of the same process. The mount phase creates the effect that drives the render pipeline. The render pipeline produces the VNode tree and patches it into the DOM. The scheduler queue decides when the effect re-runs and in what order. The mountComponent function is the entry point, setupRenderEffect is the bridge between the component instance and the reactive system, and queueJob/flushJobs are the batching mechanism that makes Vue’s updates efficient and correctly ordered. The three stages are not independent; the effect created in the mount phase is the job that the scheduler queues, and the render function executed by the effect is the pipeline that produces the DOM updates.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!