Understanding Vue 3.5 Version Counting and Lazy Update Mechanism
Vue 3.5 introduces version counting and a bidirectional linked‑list to optimize lazy updates, using a globalVersion counter, dep.version tracking, and batch processing to efficiently determine when computed and effect functions need recomputation, reducing memory usage and unnecessary calculations.
Vue 3.5 proposes two important concepts—version counting and a bidirectional linked list—as the main contributors to performance improvements in memory and computation. The article focuses on version counting, explaining its role in quickly determining whether dependencies have changed during dependency tracking.
Lazy update example:
const a = ref(0)
const b = ref(0)
const check = ref(true)
// step 1
const c = computed(() => {
console.log('computed')
if (check.value) {
return a.value
} else {
return b.value
}
})
// step 2
a.value++
effect(() => {
// step 3
console.log('effect')
c.value
c.value
})
// step 4
b.value++
// step 5
check.value= false
// step 6
b.value++The printed result is:
effect
computed
computed
effect
computed
effectThe article then explains each step, showing why the output appears as it does, emphasizing that computed functions are lazily evaluated only when their result is accessed and when their dependencies have changed.
What is lazy update? Functions like computed(fn) and effect(fn) only re‑execute their fn when the computed result is used or when any reactive dependency has been updated, avoiding unnecessary calculations.
Version counting in lazy updates involves three places in the Vue source: a global globalVersion , a version field on each Dep object, and a version field on the linked list nodes. The article walks through how trigger and track connect these counters.
globalVersion
The idea of globalVersion originated from the preact signals core. It increments whenever any reactive object changes, allowing a fast check for updates.
// file: dep.ts
/**
* reactive has an update, then globalVersion++
* provides computed with a quick way to decide if recompute is needed
*/
export let globalVersion = 0The only entry point that increments globalVersion is the trigger function, which is called whenever a Ref , Reactive , or similar value changes.
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
...
): void {
const depsMap = targetMap.get(target)
...
}If depsMap is empty, globalVersion is incremented and the function returns early because there are no dependents.
When depsMap exists, the function collects all dependent Dep objects, then calls dep.trigger() for each within a batch:
startBatch()
for (const dep of deps) {
dep.trigger()
}
endBatch()Batch processing API
Vue 3.5 introduces startBatch and endBatch to temporarily suspend effect execution while linking all effects in a chain. The actual updates happen when endBatch runs and processes the batchedEffect linked list.
export function startBatch(): void {
batchDepth++
} export function endBatch(): void {
if (--batchDepth > 0) {
return
}
while (batchedEffect) {
let e: ReactiveEffect | undefined = batchedEffect
batchedEffect = undefined
while (e) {
const next: ReactiveEffect | undefined = e.nextEffect
e.nextEffect = undefined
e.trigger()
e = next
}
}
}During dep.trigger() , both the globalVersion and the Dep 's own version are incremented, and the effect's notify method adds the effect to the batchedEffect list.
// file: effect.ts
trigger(): void {
this.version++
globalVersion++
this.notify()
}The notify method simply links the current effect into the batch chain:
notify(): void {
...
this.nextEffect = batchedEffect
batchedEffect = this
}Determining dirty state
The isDirty function checks whether any dependency version differs from the stored version or whether a computed dependency needs refreshing:
function isDirty(sub: Subscriber): boolean {
for (let link = sub.deps; link; link = link.nextDep) {
if (
link.dep.version !== link.version ||
(link.dep.computed && refreshComputed(link.dep.computed) === false)
) {
return true
}
}
return false
}For computed values, refreshComputed first compares the computed's stored globalVersion with the current globalVersion . If they differ, the computed is re‑executed; otherwise it is skipped.
export function refreshComputed(computed: ComputedRefImpl): false | undefined {
if (computed.globalVersion === globalVersion) {
return
}
computed.globalVersion = globalVersion
...
}When a computed is first run, its globalVersion is set to globalVersion - 1 so that the initial execution always proceeds.
Memory‑optimising helpers
During a computed's execution, prepareDeps resets the version of each dependent Dep to -1 , and cleanupDeps removes any Dep that remains at -1 after the function finishes, preventing stale dependencies from lingering.
// pseudo‑code
prepareDeps()
const value = computed.fn()
if (dep.version === 0 || hasChanged(value, computed._value)) {
computed._value = value
dep.version++
}
cleanupDeps()Before Vue 3.5, dependencies were stored in a plain set that had to be cleared on each run, causing frequent garbage‑collection pauses. The linked‑list approach reduces memory churn and speeds up cleanup.
Summary
The bidirectional linked list is the primary performance win in Vue 3.5, while version counting provides an additional fast‑path check. Understanding how globalVersion , dep.version , and the linked‑list interact gives a near‑complete picture of the lazy‑update optimisation.
In the linked list, the horizontal direction represents the chain of Dep nodes a subscriber ( Sub ) depends on, while the vertical direction links each reactive value’s Dep to its own subscriber chain. This structure halves memory usage compared to separate dependency sets and eliminates costly set‑clearing operations during recomputation.
I'm the "frontend snack" author; original content is hard to produce, so please follow, like, collect, and comment!
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.