Should I used @computed, @cached, or another approach?

Let’s say we have the following scenario:

import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";

export default class extends Component {
  @tracked array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

  get evens() {
    console.log("evens called");
    return this.array.filter((n) => n % 2 === 0);
  }

  @action
  updateEvens() {
    this.array = [...this.array, 12, 14, 16, 18, 20];
  }
}
<div>Length: {{evens.length}}</div>

{{#each evens as |i|}}
  <div>{{i}}</div>
{{/each}}

<button type="button" {{on "click" this.updateEvens}}>
  Click Me
</button>

With the above, I see “evens called” logged to the console twice; twice on initial render and twice when I click the button. This is happening from {{evens.length}} and {{#each evens as |i|}}.

In order for evens to be called only once on each render, I can do either of the following:

@computed("array.length")
get evens() {
  console.log("evens called");
  return this.array.filter((n) => n % 2 === 0);
}

or I can use the @cached decorator via ember-cached-decorator-polyfill:

@cached
get evens() {
  console.log("evens called");
  return this.array.filter((n) => n % 2 === 0);
}

Assuming evens was computationally expensive, would you recommend one of these approaches or something else?

1 Like

The latter (using @cached) is substantially cheaper, and it’s built on the Octane primitives, and therefore very much to be preferred. While @computed is not going anywhere anytime especially soon, the long-term goal is to remove all of the old computed property mechanics from the framework and ecosystem: they are expensive, and complicated. Implementing your own cache is also reasonable at times, for example when you know more than the autotracking system about what to think of as cache keys. I would tend to reach for @cached first, a custom cache second, and @computed never. (We’re rapidly approaching a point on the LinkedIn.com app where we will be actively migrating all classic computed properties out of existence entirely and using linting to prevent the introduction of any new uses!)

As for why @cached is cheaper: it’s precisely because it is built on the autotracking system. It will be re-executed only if tracked state consumed changes, for one thing. For another, it is guaranteed to be re-executed whenever tracked state it depends on changes: no chance of messing up the computed keys. It also can work through any number of transformations of whatever root state: no need to worry about things like the limitation of .[] or .@each. Finally, the actual caching mechanism itself is muuuuuuch cheaper: all it ever has to compare is one integer for every piece of tracked state it consumes, no matter how complex that tracked state is. By contrast, the cache comparisons when using @computed can themselves be arbitrarily expensive!

As an aside, I would be careful to identify a concrete performance problem before reaching for even @cached: caching even with autotracking still isn’t free and can sometimes still be more expensive in real-world effects on performance than just rerunning the computation. There are definitely places you want it, though, including if you’re deriving async state like a data load/API call dependent on the arguments to a component (as I covered here and here).

10 Likes

Thanks so much for that in-depth response! That was very helpful!

1 Like