Managing Derived State in Ember Octane: Getters vs. @cached vs. @computed
Learn when to use native getters, @cached, or @computed for derived state in Ember Octane to optimize rendering performance and ensure UI consistency.
09 Apr 2026, 02:38 UTC

The Challenge of Derived State
In Ember Octane, the primary goal is to ensure the UI stays in sync with the underlying data without manually triggering updates. The core problem is deciding how to express derived state—values that depend on other properties—while balancing performance and maintainability. Choosing the wrong approach can lead to unnecessary re-computations or, worse, a UI that fails to update when data changes.
Comparing Derived State Strategies
Modern Ember apps rely on autotracking, where the framework detects which @tracked properties are accessed during a render and automatically invalidates those specific template sections when the property changes. The following table compares the three primary ways to implement this logic.
| Method | Mechanism | Performance Cost | Best Use Case |
|---|---|---|---|
| Native Getter | Re-evaluates on every access | Low (if logic is simple) | Cheap derivations (e.g., string concatenation) |
| @cached | Memoizes until dependencies change | Medium (memory overhead) | Expensive calculations or stable identity needs |
| @computed | Legacy dependency keys | Variable | Legacy migration or non-tracked objects |
Trade-offs and Decision Logic
Native Getters with @tracked
This is the default choice for Octane. By using a standard JavaScript get method that reads @tracked properties, you leverage autotracking. There is no need for a decorator on the getter itself; the tracking happens at the property level. The trade-off is that the logic runs every time the template renders or the getter is called in code.
@cached from @glimmer/tracking
When a derivation involves heavy loops or complex filtering, a native getter can become a bottleneck. @cached memoizes the result. It only re-runs the logic if one of the @tracked properties accessed inside the getter changes. Note that @cached depends on the identity of the tracked property; mutating an object internally without replacing the reference may not trigger a cache invalidation.
Classic @computed
The @computed decorator is part of the legacy EmberObject system. While still supported, it uses explicit dependency keys rather than autotracking. Mixing @computed with @tracked state can lead to stale UIs because the two systems operate on different invalidation mechanisms. Use this only when interacting with legacy code or when you specifically require features like volatile or readOnly.
Implementation Example
Below is a component implementation demonstrating the difference between a simple derivation and a cached expensive operation. Ensure your package.json includes @glimmer/tracking for the @cached decorator.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { cached } from '@glimmer/tracking';
export default class UserProfileComponent extends Component {
@tracked firstName = 'Jane';
@tracked lastName = 'Doe';
@tracked filterCriteria = 'all';
// Native Getter: Cheap, re-runs every render
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
// Cached Getter: Expensive, only re-runs when dependencies change
@cached
get filteredData() {
console.log('Computing expensive filter...');
// Imagine a heavy array operation here
return someExpensiveFilter(this.filterCriteria);
}
}
Validating the Implementation
To verify that your derived state is behaving correctly, use a rendering test with ember-qunit. This ensures that autotracking is actually driving the DOM updates.
- Render the component into the test container.
- Mutate the @tracked property (e.g.,
component.firstName = 'John'). - Await
settled()to allow the Glimmer VM to process the update. - Assert the DOM: Check that the element containing
fullNamereflects the new value.
To validate @cached, add a console.log or a counter inside the getter. Trigger a render by changing an unrelated @tracked property. If the counter does not increment, the cache is working. If you change a dependency of the cached getter, the counter should increment exactly once.
Limitations and Risks
- Non-Tracked Properties: Reading a plain class field inside a getter will not trigger a re-render when that field changes. Only
@trackedproperties provide the necessary signals to the Glimmer VM. - Version Sensitivity: The availability of
@cachedvaries. Older Octane apps may use theember-cachedpolyfill. Check your lockfile to confirm if you are importing from@glimmer/tracking. - Memory: Overusing
@cachedon simple strings or numbers can actually decrease performance due to the overhead of managing the cache map.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.