Skip to content
← All questions
🔥 Advanced

How does computed object stability affect re-renders?

⚡ Performance⚛️ Reactivity👁️ Watchers

A computed property that returns a new object every time creates a new reference on each evaluation. Vue sees a new reference and triggers every watcher, effect, and child component that depends on it, even if the actual values inside the object are identical. For primitives, Vue 3.4+ handles this automatically. For objects, you need to compare manually.

The problem ​

vue
<script setup>
import { ref, computed, watchEffect } from 'vue'

const count = ref(0)

const stats = computed(() => {
  return {
    isEven: count.value % 2 === 0,
    doubled: count.value * 2
  }
})

watchEffect(() => {
  console.log('Stats changed:', stats.value)
})
</script>
Open in Vue Playground

Every time count changes, stats returns a brand new object. Vue compares by reference (===), sees a different object, and runs the effect. If count goes from 0 to 2 to 4, isEven is true all three times, but the effect fires on each change because the object reference is new.

Primitive stability (Vue 3.4+) ​

Vue 3.4 introduced automatic stability for computed properties that return primitives:

js
const count = ref(0)

const isEven = computed(() => count.value % 2 === 0)

watchEffect(() => console.log(isEven.value)) // true

count.value = 2 // isEven still true → effect does NOT run
count.value = 4 // isEven still true → effect does NOT run
count.value = 3 // isEven now false → effect runs

Vue checks oldValue === newValue internally. If the primitive hasn't changed, dependents don't re-run. This only works for primitives because {} === {} is always false.

Manual comparison for objects ​

Vue 3.4+ passes the previous value as the first argument to the computed getter:

vue
<script setup>
import { ref, computed, watchEffect } from 'vue'

const count = ref(0)

const stats = computed((oldValue) => {
  const newValue = {
    isEven: count.value % 2 === 0,
    category: count.value < 10 ? 'small' : 'large'
  }

  if (
    oldValue &&
    oldValue.isEven === newValue.isEven &&
    oldValue.category === newValue.category
  ) {
    return oldValue
  }

  return newValue
})

watchEffect(() => {
  console.log('Stats changed:', stats.value)
  // Only runs when isEven or category actually changes
})
</script>
Open in Vue Playground

Returning the old reference tells Vue nothing changed. No watchers fire, no child components re-render.

Always compute before comparing ​

The comparison must come AFTER the full computation, not before. If you return early, Vue won't track the reactive dependencies accessed during computation:

js
// BAD: early return skips dependency tracking
const result = computed((oldValue) => {
  if (oldValue && someCondition) {
    return oldValue // count.value never accessed → Vue loses the dependency
  }
  return { doubled: count.value * 2 }
})

// GOOD: compute first, then compare
const result = computed((oldValue) => {
  const newValue = { doubled: count.value * 2 } // dependency tracked
  if (oldValue && oldValue.doubled === newValue.doubled) {
    return oldValue
  }
  return newValue
})

Vue tracks dependencies during execution. If the code path skips count.value, Vue doesn't know the computed depends on count and won't re-evaluate when it changes.

Deep comparison for complex objects ​

For objects with many properties or nested structures, use a deep comparison utility:

js
import { ref, computed } from 'vue'
import { isEqual } from 'lodash-es'

const filters = ref({ category: 'all', sortBy: 'date', page: 1 })

const activeFilters = computed((oldValue) => {
  const newValue = {
    ...filters.value,
    hasFilters:
      filters.value.category !== 'all' || filters.value.sortBy !== 'date'
  }

  if (oldValue && isEqual(oldValue, newValue)) {
    return oldValue
  }

  return newValue
})

The deep comparison has its own cost, so only use it when the downstream effects are more expensive than the comparison itself.

When to split into primitives instead ​

Often the best optimization is not returning an object at all:

js
// Instead of one computed returning an object
const stats = computed(() => ({
  isEven: count.value % 2 === 0,
  doubled: count.value * 2
}))

// Split into separate primitive computeds
const isEven = computed(() => count.value % 2 === 0)
const doubled = computed(() => count.value * 2)

Each primitive computed gets Vue 3.4+ automatic stability for free. Components that only use isEven won't re-render when doubled changes.

Comparison ​

ApproachStabilityEffort
Object computed (default)None, new reference every timeZero
Primitive computed (Vue 3.4+)AutomaticZero
Object computed with manual comparisonStable when values matchShallow comparison code
Object computed with deep comparisonStable for nested objectslodash/custom utility
Split into primitive computedsAutomatic per propertyRestructure consuming code

See also: Why does my computed property not update when a dependency changes? · How does Vue batch DOM updates?

References ​

Released under the MIT License.