Mastering Ember Octane’s @tracked: Avoiding Common Pitfalls and Boosting Performance
Learn how to use Ember Octane’s @tracked decorator effectively, avoid mutation pitfalls, and optimize component performance with a concrete counter example.
03 Jun 2026, 04:54 UTC

Why Re‑reactivity Matters in Ember Octane Components
When you build a UI with Ember Octane, the framework’s rendering engine only re‑renders a component when it knows that something the component uses has changed. The @tracked decorator tells Ember to watch a property and schedule a re‑render when the property’s reference changes. Without it, the component will never update after the initial render, even if you mutate a value inside the component.
Thesis: @tracked Gives Fine‑Grained Reactivity, But Requires Careful State Management
The power of @tracked lies in its simplicity: add the decorator, and Ember handles the rest. However, this simplicity can hide subtle bugs. If you mutate an object or array directly, Ember won’t see the change because the reference stays the same. Likewise, declaring tracked properties in a way that shares state across instances can lead to hard‑to‑track bugs. The key is to treat tracked properties as immutable references and replace them when you need to change the underlying data.
Declaring Tracked Properties
- Declare them as class fields or inside the constructor to avoid accidental sharing.
export default class Counter extends Component { @tracked count = 0; // fine } - Do not put
@trackedon a prototype property that is shared across all instances.
Avoiding Mutation Pitfalls
- Direct mutation of objects or arrays does not trigger a re‑render.
// ❌ Won’t update the UI this.items.push('new'); - Replace the reference to trigger reactivity.
// ✅ Triggers a re‑render this.items = [...this.items, 'new']; - For nested objects, use Ember’s
sethelper or replace the entire object.
Performance Considerations
Each @tracked property creates an observation that runs on every render cycle. In large components, over‑tracking can cause noticeable performance hits. A common strategy is to group related state into a single tracked object and replace that object when any piece changes.
| Approach | Pros | Cons |
|---|---|---|
| One tracked property per field | Fine‑grained updates | Higher observation overhead |
| Single tracked object | Lower overhead | More work to replace the object |
Concrete Example: A Reactive Counter Component
Below is a minimal Ember Octane component that demonstrates correct usage of @tracked, safe mutation, and performance awareness.
// app/components/reactive-counter.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class ReactiveCounter extends Component {
@tracked count = 0;
@tracked items = [];
increment() {
// Replace the reference to trigger re‑render
this.count = this.count + 1;
}
addItem() {
// Replace the array reference, not mutate in place
this.items = [...this.items, `Item ${this.count}`];
}
}
{{!-- app/templates/components/reactive-counter.hbs --}}
<div>
<button type="button" {{on "click" this.increment}}>Increment</button>
<button type="button" {{on "click" this.addItem}}>Add Item</button>
<p>Count: {{this.count}}</p>
<ul>
{{#each this.items as |item|}}
<li>{{item}}</li>
{{/each}}
</ul>
</div>
Running this component in a browser shows the counter and list updating instantly after each button click. The Ember Inspector’s Performance tab will reveal that only the minimal parts of the component re‑render, not the entire tree.
Trade‑Off: Over‑Tracking vs. Grouping State
Choosing the right granularity of tracked properties is a balancing act. Over‑tracking can lead to frequent re‑renders and higher CPU usage, especially in components that update often. Grouping state into a single object reduces observation overhead but can make debugging harder because a change to any field requires replacing the whole object. A pragmatic approach is to start with individual tracked fields for clarity, then refactor to grouped state only when profiling shows a bottleneck.
Actionable Checklist Before You Commit
- Declare
@trackedonly on class fields or constructor properties. - Never mutate tracked objects or arrays in place; always replace the reference.
- Use Ember Inspector’s Performance tab to count renders after each change.
- Profile large components; if render counts spike, consider grouping related state.
- Test asynchronous actions (e.g., fetch) to ensure they replace tracked references, not mutate.
By following these guidelines, you’ll harness Ember Octane’s reactive system reliably, avoid the most common pitfalls, and keep your UI snappy.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.