Frontend Development 15 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Understanding Vue 3.5 Version Counting and Lazy Update Mechanism

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
effect

The 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 = 0

The 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!
performance optimizationVuedependency trackingLazy UpdateReactive SystemVersion Counting
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.