Caching Getters in Ember Octane

Last reviewed on December 13, 2020

This post is based off a question I had asked in the Ember Discuss forum about the caching of getters.

Recently I discovered that getters will execute whenever they are accessed, even if the dependent properties haven't changed. Ember doesn't do any caching on getters, unlike with computed properties.

Let's say we have the following Glimmer component:

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

export default class extends Component {
  @tracked array = [1, 2, 3, 4, 5];

  get evens() {
    console.log("evens called");
    return this.array.filter(n => n % 2 === 0);
  }
}
<div>Length: {{this.evens.length}}</div>

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

With the above, we will see "evens called" logged to the console twice when the component is rendered. This is happening because of {{this.evens.length}} and {{#each this.evens as |i|}} in our template.

Now in this trivial example, filtering this.array for the even numbers isn't a performance problem. However, we can imagine a performance problem creeping in if the evens getter was computationally expensive. Displaying the length of some array and iterating over that same array to display each item seems like a pretty common scenario. So how can we change this so that the evens getter is cached and only recomputes if this.array changes?

One approach is to use the @computed decorator:

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

export default class extends Component {
  @tracked array = [1, 2, 3, 4, 5];

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

Now, the evens getter will only be called once instead of twice when the component is rendered.

Another approach is to use the @cached decorator, which is a newly proposed decorator in RFC 566 which I highly recommend reading. Luckily, there is an addon that polyfills @cached called ember-cached-decorator-polyfill. Usage would look like this:

import Component from "@glimmer/component";
import { tracked, cached } from "@glimmer/tracking";

export default class extends Component {
  @tracked array = [1, 2, 3, 4, 5];

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

By using the @cached decorator, the evens getter will also only be called once instead of twice when the component is rendered.

From a DX perspective, I think @cached is a really nice improvement over @computed because I don't have to list out the dependent keys. It is very easy to accidentally forget or mispell a key, which can create subtle bugs.

Chris Krycho who is leading the Octane migration for Linkedin.com was kind enough to give an in-depth response on my post as to whether using @computed, @cached, or another approach is best.

To summarize:

  1. Using @cached is recommended because it is substantially cheaper since it is built on the autotracking system whereas @computed isn't.
  2. The long-term goal is to remove the computed property mechanics from the framework.
  3. Carefully identify if there is a performance problem before reaching for @cached, as it still isn't free and can be more expensive than rerunning the computation.

Regarding point #3, it's also worth noting that the @cached RFC also mentions in the Drawbacks section that this decorator should be avoided when possible and the framework plans to discourage overuse.