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: </div>
<div></div>
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:
- Using
@cached
is recommended because it is substantially cheaper since it is built on the autotracking system whereas@computed
isn't. - The long-term goal is to remove the computed property mechanics from the framework.
- 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.