Skip to content
← Todas las preguntas
🔥 Avanzado

¿Cómo afecta la estabilidad de objetos en computed a los re-renders?

⚡ Rendimiento⚛️ Reactividad👁️ Watchers

Una propiedad computed que devuelve un objeto nuevo en cada evaluación crea una nueva referencia en cada ejecución. Vue detecta la nueva referencia y dispara cada watcher, efecto y componente hijo que dependa de ella, aunque los valores dentro del objeto sean idénticos. Para primitivos, Vue 3.4+ lo gestiona automáticamente. Para objetos, hay que comparar de forma manual.

El problema ​

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

Cada vez que count cambia, stats devuelve un objeto completamente nuevo. Vue compara por referencia (===), detecta un objeto diferente y ejecuta el efecto. Si count pasa de 0 a 2 a 4, isEven es true las tres veces, pero el efecto se dispara en cada cambio porque la referencia del objeto es nueva.

Estabilidad de primitivos (Vue 3.4+) ​

Vue 3.4 introdujo estabilidad automática para propiedades computed que devuelven primitivos:

js
const count = ref(0)

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

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

count.value = 2 // isEven sigue siendo true → el efecto NO se ejecuta
count.value = 4 // isEven sigue siendo true → el efecto NO se ejecuta
count.value = 3 // isEven ahora es false → el efecto se ejecuta

Vue comprueba oldValue === newValue internamente. Si el primitivo no ha cambiado, los dependientes no se vuelven a ejecutar. Esto solo funciona con primitivos porque {} === {} siempre es false.

Comparación manual para objetos ​

Vue 3.4+ pasa el valor anterior como primer argumento al getter del computed:

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)
  // Solo se ejecuta cuando isEven o category cambian de verdad
})
</script>
Open in Vue Playground

Devolver la referencia antigua le indica a Vue que no hubo cambios. No se disparan watchers ni se re-renderizan componentes hijos.

Calcular siempre antes de comparar ​

La comparación debe hacerse DESPUÉS del cálculo completo, no antes. Si se devuelve antes de tiempo, Vue no registrará las dependencias reactivas accedidas durante el cálculo:

js
// MAL: el retorno anticipado omite el seguimiento de dependencias
const result = computed((oldValue) => {
  if (oldValue && someCondition) {
    return oldValue // count.value nunca se accede → Vue pierde la dependencia
  }
  return { doubled: count.value * 2 }
})

// BIEN: calcular primero, luego comparar
const result = computed((oldValue) => {
  const newValue = { doubled: count.value * 2 } // dependencia registrada
  if (oldValue && oldValue.doubled === newValue.doubled) {
    return oldValue
  }
  return newValue
})

Vue registra las dependencias durante la ejecución. Si la ruta de código omite count.value, Vue no sabe que el computed depende de count y no lo re-evaluará cuando cambie.

Comparación profunda para objetos complejos ​

Para objetos con muchas propiedades o estructuras anidadas, usar una utilidad de comparación profunda:

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
})

La comparación profunda tiene su propio coste, así que conviene usarla solo cuando los efectos posteriores son más caros que la comparación en sí.

Cuándo dividir en primitivos ​

Con frecuencia la mejor optimización es no devolver un objeto:

js
// En lugar de un computed que devuelva un objeto
const stats = computed(() => ({
  isEven: count.value % 2 === 0,
  doubled: count.value * 2
}))

// Dividir en computeds primitivos independientes
const isEven = computed(() => count.value % 2 === 0)
const doubled = computed(() => count.value * 2)

Cada computed primitivo obtiene la estabilidad automática de Vue 3.4+ sin coste adicional. Los componentes que solo usen isEven no se re-renderizarán cuando doubled cambie.

Comparativa ​

EnfoqueEstabilidadEsfuerzo
Object computed (por defecto)Ninguna, nueva referencia siempreCero
Primitive computed (Vue 3.4+)AutomáticaCero
Object computed con comparación manualEstable cuando los valores coincidenCódigo de comparación superficial
Object computed con comparación profundaEstable para objetos anidadoslodash o utilidad personalizada
Dividir en computeds primitivosAutomática por propiedadReestructurar el código consumidor

Ver también: ¿Por qué mi propiedad computed no se actualiza cuando cambia una dependencia? · ¿Cómo agrupa Vue las actualizaciones del DOM?

Referencias ​

Publicado bajo la licencia MIT.