DEV Community

loading...
Cover image for When to avoid VueJs Computed Properties for greater performance

When to avoid VueJs Computed Properties for greater performance

Matteo Fogli
CEO of @madebymodo, dev, obssessed with performance, promoting a faster and universal Web
・7 min read

VueJs is fast, easy to learn, reactive framework. It offers a gentle learning curve and a rich developer experience, with powerful inspection and CLI based tools.

At modo, we use Vue for most of our dynamic frontend projects: we like it for its clear separation of concerns, easily composable components, a wide range of build options (from Single Page Applications to standalone Web Components) and for being generally very well performing out–of–the–box.

Quick links

Computed Properties

Among the many traits of VueJs that are optimized for computational and rendering performance are computed properties. Computed properties are component functions that return a value. They are reactive, in that they are automatically updated whenever one of the values used for computing the property changes, and can be used in templates and component logic.

The real power (and the true difference in comparison to component methods) is that computed properties are cached.

While component methods are executed at every rendering of the component, computed properties are evaluated conditionally: if none of the dependency has changed, the property is not recomputed and the cache value is used, saving a considerable amount of runtime execution. Component re–rendering thus happens considerably faster, as the runtime engine can skip re–evaluating potentially expensive data, relying instead on the cached value.

If you’re not familiar with computed properties, you can read more in the official documentation, but for the sake of this article a simple use case can help.

Suppose we have a list of books, and a component that should filter them based on a condition, let’s say a matching text in the title.

Our code would probably look like this:

<template>
  <section class="c-book">
    <h2>Your search for {{ bookQuery }} returned {{ filteredBookList.length }} books</h2>
    <ul v-if="filteredBookList.length" class="c-book__list">
      <li v-for="book in filteredBookList" :key="book.id">
        {{ book.title }}
      </li>
    </ul>
  </section>
</template>
<script>
export default {
  name: 'BookFilter',

  props: {
    bookList: {
      type: Array,
      required: true,
    },
    bookQuery: {
      type: String,
      default: '',
    },
  },

  computed: {
    filteredBookList () {
      return this.bookList.filter(book => book.title.includes(this.bookQuery))
    },
  },
}
</script>

Our component and application would probably have additional logic and greater complexity, but this example should be good enough to show how computed properties work. filteredBookList returns a copy of the array of books, filtered with those that include the search query (bookQuery) in their title. The value is then cached by Vue, and will not be recomputed unless either bookList or bookQuery change.

An important tidbit to remember, tightly coupled with their cacheability, is that computed properties must be evaluated synchronously.

What happens inside computed properties

We’re not going to deep dive into Vue internals. If you are interested, VueMastery has produced a series of videos with Evan You coding step–by–step, high level, simplified demos of how VueJs works. You can also inspect Vue source code and, for this specific case, I found this article by Michael Gallagher very inspiring.

So, to make it short, in version 2.x, Vue tracks reactivity using getters and setters (Version 3 will use proxies, and will also provide better tracing and debugging of reactivity. It is currently a Release Candidate).

To understand why computed properties can bite back in specific cases, we need to remember that Vue needs to track each dependency of a computed property. This can be expensive if these variables are large arrays, or complex, nested objects, or a combination of both, as in the case I found out.

In case of circular dependencies (a computed property depending on another computed property), Vue also needs to stabilize values and might have to iterate the evaluation twice to ensure that values don’t change within the current update cycle.

All of this can add up significantly.

How and when to avoid computed properties

Despite all the praises I’ve written so far for computed properties, there are cases in which you might want or even need to avoid them.

The simplest case is if you need a static computed value, that is a value that needs to be calculated only once, no matter how data in your component will change.

The options API of VueJs 2.x does not make it particularly evident, but declaring any property inside the component created function makes it available to both the component logic and the component template.

Let’s see this in code, with our book example:

<template>
  <section class="c-book">
    <h2>Your search for {{ bookQuery }} returned {{ filteredBookList.length }} books</h2>
    <ul v-if="filteredBookList.length" class="c-book__list">
      <li v-for="book in filteredBookList" :key="book.id">
        {{ book.title }}
      </li>
    </ul>
  <footer>
    {{ productName }} v{{ productVersion }}
  </footer>
  </section>
</template>
<script>
// let’s import the name and version number from package.json
import { version: productVersion, name: productName } from "../package.json";

export default {
  name: 'BookFilter',

  props: {
    bookList: {
      type: Array,
      required: true,
    },
    bookQuery: {
      type: String,
      default: '',
    },
  },

  computed: {
    filteredBookList () {
      return this.bookList.filter(book => book.title.includes(this.bookQuery))
    },
  },

  created() {
    // once the component has been created, we can add non reactive properties to it.
    // this data is static and will never be tracked by Vue
    this.productVersion = productVersion;
    this.productName = productName;
  }
}
</script>

See this example on CodeSandbox

In the above example, the product name and version are completely static. They are imported from the package.json file and used in the template as if they were any other computed or reactive property.

You could still define productVersion and productName as computed properties: Vue would not track values that are not registered in the reactive system, but this approach becomes useful when you need to explicitely avoid having Vue track a large array or object.

In our example above, bookList is reactive. It is passed to the component as a prop, and therefore tracked for reactivity changes. The component needs to update should the list change. If the list is very large and includes complex objects, we’re adding an unnecessary overhead to the reactivity system. We’re not reacting to changes in bookList: we’re reacting only to changes in bookQuery! The list of books stays the same, no matter the search the user performs.

This might not be intuitive, and most of the time Vue is forgiving because its internal optimizations favour speed. But every time we know that some data does not need to ever be re–evaluated, we should design for performance and make it available as a static property.

See the final example on CodeSandbox

Going from a few milliseconds to tens of seconds of execution (and back)

The book example described above has many similarities with a project I worked on recently. This project needed to handle an array of around 10.000 objects, and filter them dynamically based on various query types.

In a particular scenario, the performance of the app degraded significantly, blocking the browser main thread for several seconds and making the browser completely unresponsive. Granted, the app was managing a lot of data: the JSON representation of the array was ~19Mb of data. But the slowdown was not due to the data size.

The code for filtering the array was not at fault. An isolated case in plain JavaScript performing the same filtering took only a few milliseconds, no matter the code style (functional or imperative) or the complexity of the filter function.

the greatest advantage of a computed property can sometime become its greatest defect

To troubleshoot the issue, I used the performance profiler of Firefox DevTools to generate a flame chart. This showed where the application was spending time in long tasks that blocked the browser main thread. Reading the chart provided some insight: it turned out that the greatest advantage of computed properties sometimes becomes a performance bottleneck.

A flame chart from Firefox DevTools

Tracking dependencies has a cost. Most of the time this cost is negligible, especially compared to the advantage provided by cached values and a fast, reactive frontend. In our app, this cost was acceptable when the component tracked in total one list of items. But when tracking two, and combining this cost with the cost generated on the heap and the call stack by a high number of callback functions—such as the one used to filter() a large array—it blocked the browser event loop and made the application unresponsive.

To solve it, as explained above, we removed the list of items to search from the reactivity system. In retrospect it was a mistake to add it from the beginning, since it never changes and never needs to be tracked for updates. The array is now filtered once inside the created() function and the resulting value added as a non–reactive property to the component. We’re back in the milliseconds range of updates. Yeah! 🍾

Performance by design

Despite working with VueJs for years, this was the first time we hit a performance penalty this severe. VueJs 3.x has a lot of internal improvements and might perform flawlessly in this scenario (we haven’t tested it yet). But if you’re still relying on VueJs 2, and like us are obsessed with performance, I hope this helps you if you ever experience an unexpected slowdown in your VueJs code. And if it’s really puzzling, you might want to let us take a look at it.

Cover photo by Heye Jensen on Unsplash

Discussion (2)

Collapse
raheel98 profile image
Raheel Khan

Another way to turn off reactivity on arrays or objects is to call Object.freeze() on it.

You can also use the v-once directive for a one time binding so that the dom element doesn't track the property for changes.

Collapse
pecus profile image
Matteo Fogli Author

Good point! That’s indeed correct. Both are mentioned in the documentation and serve different purposes. v-once removes the node from diffing in virtual-DOM, while Object.freeze() effectively prevents from tracking reactivity (akin to what was exemplified in the article).