Vue: When a computed property can be the wrong tool

If you’re a Vue user, you likely know computed properties, and if you are like me, you probably think they are awesome – rightfully so!

To me, computed properties are a very ergonomic and elegant way to deal with derived state – that is: state which i…


This content originally appeared on DEV Community and was authored by Thorsten Lünborg

If you're a Vue user, you likely know computed properties, and if you are like me, you probably think they are awesome - rightfully so!

To me, computed properties are a very ergonomic and elegant way to deal with derived state - that is: state which is made up from other state (its dependencies). But in some scenarios, they can also have a degrading effect on your performance, and I realized that many people are unaware of that, so this is what this article will attempt to explain.

To make clear what we are talking about when we say "computed properties" in Vue, here's a quick example:

const todos = reactive([
  { title: 'Wahs Dishes', done: true},
  { title: 'Throw out trash', done: false }
])

const openTodos = computed(
  () => todos.filter(todo => !todo.done)
)

const hasOpenTodos = computed(
  () => !!openTodos.value.length
)

Here, openTodos is derived from todos, and hasOpenTodos is derived from openTodos. This is nice because now we have reactive objects that we can pass around and use, and they will automatically update whenever the state that they depend on, changes.

If we use these reactive objects in a reactive context, such as a Vue template, a render function or a watch(), these will also react to the changes of our computed property and update - that's the magic at the core of Vue that we value so much, after all.

Note: I'm using composition API because that's what I like to use these days. The behaviors describes in this article apply to computed properties in the normal Options API just as much, though. Both use the same reactivity system, after all.

What is special about computed properties

There's two things about computed properties that make them special and they are relevant to the point of this article:

  1. Their results are cached and only need to be re-evaluated once one of its reactive dependencies changes.
  2. They are evaluated lazily on access.

Caching

A computed property's result is cached. In our example above, that means that as long as the length of todos doesn't change, calling openTodos.value will return the same value without re-running the filter method. This is especially great for expensive tasks, as this ensures that the task is only ever re-run when it has to – namely when one of its reactive dependencies has changed.

Lazy Evaluation

Computed properties are also evaluated lazily – but what does that mean, exactly?

It means that the callback function of the computed property will only be run once the computed's value is being read (initially or after it was marked for an update because one of its dependencies changed).

So if a computed property with an expensive computation isn't used by anything, that expensive operation won't even be done in the first place - another performance benefit when doing heavy lifting on a lot of data.

When lazy evaluation can improve performance

As explained in the previous paragraph, lazy evaluation of computed properties is a usually a good thing, especially for expensive operations: It ensures that the evaluation is only ever done when the result is actually needed.

This means that things like filtering a big list will simply be skipped if that filtered result won't be read and used by any part of your code at that moment. Here's a quick example:

<template>
  <input type="text" v-model="newTodo">
    <button type="button" v-on:click="addTodo">Save</button>
  <template v-if="showListView">
    <h2>{{ openTodos.length }}</h2> Todos:
      <ul>
            <li v-for="todo in openTodos">
          {{ todo.title }}
        </li>
      </ul>
    </template>
</template>

<script setup>
const showListView = ref(false)

const todos = reactive([
  { title: 'Wahs Dishes', done: true},
  { title: 'Throw out trash', done: false }
])
const openTodos = computed(
  () => todos.filter(todo => !todo.done)
)
const hasOpenTodos = computed(
  () => !!openTodos.value.length
)

const newTodo = ref('')
function addTodo() {
  todos.push({
    title: todo.value,
    done: false
  })
}
</script>

See This code running on the SFC Playground

Since showList is initially false, the template/render function will not read openTodos, and consequently, the filtering would not even happen, neither initially nor after a new todo has been added and todos.length has changed. Only after showList has been set to true, these computed properties would be read and that would trigger their evaluation.

Of course in this small example, the amount of work for filtering is minimal, but you can imagine that for more expensive operations, this can be a huge benefit.

When lazy evaluation can degrade performance

There is a downside to this: If the result returned by a computed property can only be known after your code makes use of it somewhere, that also means that Vue's Reactivity system can't know this return value beforehand.

Put another way, Vue can realize that one or more of the computed property's dependencies have changed and so it should be re-evaluated the next time it is being read, but Vue can't know, at that moment, wether the result returned by the computed property would actually be different.

Why can this be a problem?

Other parts of your code may depend on that computed property – could be another computed property, could be a watch(), could be the template/render function.

So Vue has no choice but to mark these dependents for an update as well – "just in case" the return value will be different.

If those are expensive operations, you might have triggered an expensive re-evaluation even though your computed property returns the same value as before, and so the re-evaluation would have been unnecessary.

Demonstrating the issue

Here's a quick example: Imagine we have a list of items, and a button to increase a counter. Once the counter reaches 100, we want to show the list in reverse order (yes, this example is silly. Deal with it).

(You can play with this example on this SFC playground)

<template>
  <button @click="increase">
    Click me
  </button>
  <br>
  <h3>
    List
  </h3>
  <ul>
    <li v-for="item in sortedList">
      {{ item }}
    </li>
  </ul>
</template>

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

const list = reactive([1,2,3,4,5])

const count = ref(0)
function increase() {
  count.value++
}

const isOver100 = computed(() => count.value > 100)

const sortedList = computed(() => {
  // imagine this to be expensive
  return isOver100.value ? [...list].reverse() : [...list]
})

onUpdated(() => {
  // this eill log whenever the component re-renders
  console.log('component re-rendered!')
})
</script>

Question: You click the button 101 times. How often does our component re-render?

Got your answer? You sure?

Answer: It will re-render 101 times*.*

I suspect some of you might have expected a different answer, something like: "once, on the 101st click". But that's wrong, and the reason for this is the lazy evaluation of computed properties.

Confused? We'll walk through what's happening step by step:

  1. When we click the button, the count is increased. The component would not re-render, because we don't use the counter in the template.
  2. But since count changed, our computed property isOver100is marked as "dirty" - a reactive dependency changed, and so its return value has to be re-evaluated.
  3. But due to lazy evaluation, that will only happen once something else reads isOver100.value - before that happens, we (and Vue) don't know if this computed property will still return false or will change to true.
  4. sortedListdepends on isOver100 though - so it also has to be marked dirty. And likewise, it won't yet be re-evaluated because that only happens when it's being read.
  5. Since our template depends on sortedList, and it's marked as "dirty" (potentially changed, needs re-evaluation), the component re-renders.
  6. During rendering, it reads sortedList.value
  7. sortedList now re-evaluates, and reads isOver100.value – which now re-evaluates, but still returns false again.
  8. So now we have re-rendered the component and re-run the "expensive" sorteListcomputed even though all of that was unnecessary - the resulting new virtual DOM / template will look exactly the same.

The real culprit is isOver100 – it is a computed that often updates, but usually returns the same value as before, and on top of that, it's a cheap operation that doesn't really profit from a the caching computed properties provide. We just used a computed because it feels ergonomic, it's "nice".

When used in another, expensive computed (which does profit from caching) or the template, it will trigger unnecessary updates that can seriously degrade your code's performance depending on the scenario.

It's essentially this combination:

  1. An expensive computed property, watcher or the template depends on
  2. another computed property that often re-evaluates to the same value.

How to solve this problem when you come across it.

By now you might have two questions:

  1. Wow! Is this a bad problem?
  2. How do I get rid of it?

So first off: Chill. Usually, this is not a big problem. Vue's Reactivity System is generally very efficient, and re-renders are as well, especially now in Vue 3. usually, a couple unnecessary updates here and there will still perform much better than a React counterpart that by default, re-renders on any state change whatsoever.

So the problem only applies to specific scenarios where you have a mix of frequent state updates in one place, that trigger frequent unnecessary updates in another place that is expensive (very large component, computationally heavy computed property etc).

If you encounter such a situation, luckily you have different ways of solving it:

  1. Using plain functions instead of standalone computed properties
  2. Using Getters instead of computed properties on objects
  3. Using a custom "eagerly computed" property

Plain functions

If the operation of our computed property is a cheap one-liner, we can use a function instead:

// computed property
const hasOpenTodos = computed(() => !!todos.length)
// usage
if (hasOpenTodos.value) {
  // list open todos
}

// Simple function
const hasOpenTodos = () => !!todos.length
// Usage
if (hasOpenTodos()) {
  // list open todos
}

Both ways offer a descriptive naming, but the second one is likely a bit better for overall performance, as a simple function is lighter on memory and CPU usage that a computed property, and its operation – reading the length of an array – is so cheap that the computed's cache behavior won't offer any benefit over this.

And a simple function won't have lazy evaluation, so we don't risk triggering unnecessary effect runs of the template/render function, watcher or another computed property.

Now, in most circumstances, this might not have a big impact, but in certain scenarios, it might have. Just imagine a component that uses several of this kind of computed property and is being render many times in a big list – here, using functions instead of a computed property can save you some memory for sure.

I'd say that using a computed property is still fine in close to all cases on its own. If you prefer the style of a computed property over the simple function, by all means just do what you prefer.

Getters

I have also seen a pattern like this one being used in the wild:

import { reactive, computed } from 'vue'
const state = reactive({
  name: 'Linusborg',
  bigName: computed(() => state.name.toUpperCase())
})

This can be handy if you want to have an object where some properties are derived from others.

But actually, a computed property would be overkill in this example. Javascript has its own way of deriving state for an object property – called Getters. It doesn't have caching or lazy evaluation, but that's fine here, we don't really profit much from those in such a scenario anyway.

import { reactive } from 'vue'
const state = reactive({
  name: 'Linusborg',
  get bigName() { return state.name.toUpperCase() )
})

Problem solved.

Custom eagerComputed helper

Functions and getters are fine, but for those of us who are used to Vue's way of doing things, a computed property may just feel nicer. Luckily, Vue's Reactivity System gives us all of the required tools to build our own version of a computed(), one that evaluates eagerly, not lazily.

Let's call it eagerComputed()

import { watchEffect, shallowRef, readonly } from 'vue'
export function eagerComputed(fn) {
  const result = shallowRef()
  watchEffect(() => {
    result.value = fn()
  })

  return readonly(result)
}

We can then use this like we would use a computed property, but the difference in behavior is that the update will be eager, not lazy, getting rid of unnecessary updates.

const hasOpenTodos = eagerComputed(() => !!todos.length)

When would you use computed() and when eagerComputed()?

  • Use computed()when you have a complex calculation going on, which can actually profit from caching and lazy evaluation and should only be (re-)calculated if really necessary.
  • Use eagerComputed() when you have a simple operation, with a rarely changing return value – often a boolean.

Finishing up

So this is it. We dove deeper into how computed properties actually work. We learned when they are beneficial for your app's performance, and when they can degrade it. Concerning the latter scenario, we covered 3 different ways to solve the performance problem by avoiding unnecessary reactive updates.

I hope this was helpful. Let me know if you have questions, and tell me other topics you may want me to cover.


This content originally appeared on DEV Community and was authored by Thorsten Lünborg


Print Share Comment Cite Upload Translate Updates
APA

Thorsten Lünborg | Sciencx (2021-09-05T12:57:45+00:00) Vue: When a computed property can be the wrong tool. Retrieved from https://www.scien.cx/2021/09/05/vue-when-a-computed-property-can-be-the-wrong-tool/

MLA
" » Vue: When a computed property can be the wrong tool." Thorsten Lünborg | Sciencx - Sunday September 5, 2021, https://www.scien.cx/2021/09/05/vue-when-a-computed-property-can-be-the-wrong-tool/
HARVARD
Thorsten Lünborg | Sciencx Sunday September 5, 2021 » Vue: When a computed property can be the wrong tool., viewed ,<https://www.scien.cx/2021/09/05/vue-when-a-computed-property-can-be-the-wrong-tool/>
VANCOUVER
Thorsten Lünborg | Sciencx - » Vue: When a computed property can be the wrong tool. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/09/05/vue-when-a-computed-property-can-be-the-wrong-tool/
CHICAGO
" » Vue: When a computed property can be the wrong tool." Thorsten Lünborg | Sciencx - Accessed . https://www.scien.cx/2021/09/05/vue-when-a-computed-property-can-be-the-wrong-tool/
IEEE
" » Vue: When a computed property can be the wrong tool." Thorsten Lünborg | Sciencx [Online]. Available: https://www.scien.cx/2021/09/05/vue-when-a-computed-property-can-be-the-wrong-tool/. [Accessed: ]
rf:citation
» Vue: When a computed property can be the wrong tool | Thorsten Lünborg | Sciencx | https://www.scien.cx/2021/09/05/vue-when-a-computed-property-can-be-the-wrong-tool/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.