Vue nextTick & Asynchronous Update Batching
When you mutate reactive state in Vue, the DOM is not updated synchronously. Instead, Vue batches mutations and updates the DOM asynchronously inside a single "tick" using the browser event loop's microtask queue.
1. What the Interviewer Is Testing
Interviewers use this question to check if you understand:
- Asynchronous Update Loop: Why does Vue delay updating the DOM?
- Javascript Event Loop: How does Vue schedule updates using microtask queues (
Promise.then)? - nextTick Execution: What is the correct timing for reading DOM layouts after state mutations?
- Performance Bottlenecks: How batching prevents layout thrashing (forced synchronous layout).
2. Why Vue Batches Updates
If you mutate state multiple times in a loop, updating the DOM synchronously on every single state change would cause terrible rendering performance.
// Example: Modifying count 1000 times
for (let i = 0; i < 1000; i++) {
count.value = i
}- Synchronous rendering: The browser would be forced to re-calculate styles and layout 1000 times sequentially, blocking the main thread.
- Vue Asynchronous Batching: Vue intercepts the setter calls, pushes the reactive effects to a global de-duplicated queue (
scheduler.ts), and schedules a single DOM draw at the end of the current microtask tick. The browser renders the screen only once.
3. How nextTick Works
nextTick is a global utility helper that returns a Promise. It resolves immediately after the current DOM update cycle has completed.
<script setup>
import { ref, nextTick } from 'vue'
const message = ref('Hello')
const messageEl = ref(null)
async function updateMessage() {
message.value = 'Hello World'
// ❌ Fails: The DOM has not updated yet!
console.log(messageEl.value.textContent) // Logs: 'Hello'
// Wait for the update cycle to complete
await nextTick()
// ✅ Success: DOM is now updated!
console.log(messageEl.value.textContent) // Logs: 'Hello World'
}
</script>
<template>
<p ref="messageEl">{{ message }}</p>
<button @click="updateMessage">Update</button>
</template>4. Under the Hood: The Microtask Scheduler
Vue schedules the flush of its update queues using JavaScript Promises (microtasks).
// Simplified nextTick and Scheduler implementation
const queue = []
let isFlushing = false
const resolvedPromise = Promise.resolve()
function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job)
queueFlush()
}
}
function queueFlush() {
if (!isFlushing) {
isFlushing = true
// Push the flush operation to the microtask queue
resolvedPromise.then(flushJobs)
}
}
function flushJobs() {
try {
for (let i = 0; i < queue.length; i++) {
queue[i]() // Run updates & re-render DOM
}
} finally {
queue.length = 0
isFlushing = false
}
}
export function nextTick(fn) {
const p = resolvedPromise
return fn ? p.then(fn) : p
}Event Loop Timeline
- State Mutation:
count.value++is executed on the stack. - Queue Job: The reactive effect triggers setter proxy -> pushes DOM update callback to the scheduler
queue. - Schedule Microtask: Vue pushes
flushJobscallback to the browser's Microtask Queue using.then(). - Execute Call Stack: Synchronous JavaScript operations continue until stack is empty.
- Flush Microtasks: Browser pulls
flushJobsfrom microtask queue -> renders all batch updates. - Resolve nextTick:
nextTickPromise resolves, running your post-update code.
