Lesson 25 of 30
OnPush narrows down which components get checked; @for's mandatory track expression narrows down which DOM nodes get touched when a list changes. Together they're most of what 'fast Angular' means.
16 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Two separate decisions drive most of the real-world performance work in a signals-first Angular app.
The first is ChangeDetectionStrategy.OnPush. An OnPush component only gets checked when one of three things happens: an @Input reference changes (by ===, not deep equality), an event originates from inside the component itself, or — in a signals-first app — a signal the template reads changes value. That last one is the important addition: reading count() in a template wires the component directly into the signal graph, so count.set() schedules exactly that component, no OnPush polling or dirty-checking involved.
The second is the track expression on @for. Angular needs to know how to match array items across re-renders — same items reordered, or genuinely different items — so it can patch just the DOM that actually changed instead of tearing everything down and rebuilding. track is how you tell it. Unlike the old *ngFor with an optional trackBy function, @for requires a track expression; there's no way to skip it and quietly fall back to something worse.
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
interface Todo {
id: string;
text: string;
done: boolean;
}
@Component({
selector: 'app-todo-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<ul>
@for (todo of todos(); track todo.id) {
<li [class.done]="todo.done">{{ todo.text }}</li>
}
</ul>
`,
})
export class TodoListComponent {
todos = input.required<Todo[]>();
}
track todo.id tells Angular to match list items by their stable id, not by position. Reorder the array, and Angular moves the existing <li> elements to match — it doesn't destroy and recreate them. That matters for more than raw speed: destroying and recreating an element loses whatever DOM state it was holding — focus, in-progress CSS transitions, scroll position inside it. track todo.id keeps identity stable across the reorder, so none of that gets lost.
OnPush on the component means this whole check only runs when todos gets a new array reference (a new @Input/signal input value) or an event fires inside TodoListComponent. If a parent mutates the array in place — todos.push(newTodo) without creating a new array — OnPush won't see a reference change and won't re-check at all. That's a real gotcha, not just a footnote: signals and immutable updates (.set()/.update() producing a new array) are what make OnPush actually work as intended.
track $index out of habit. That's precisely the identity confusion track was introduced to prevent — for any list that reorders, filters, or inserts, index-based tracking either forces unnecessary DOM rebuilds or, worse, silently reuses element state across genuinely different items.OnPush. todos().push(x) doesn't change the array reference, so OnPush sees nothing different and skips the check. You need a new reference — a new array via spread, or a signal's .set()/.update() — for the change to actually register.OnPush, signal-driven, correctly-track-ed app patches exactly the DOM that changed on every interaction, rather than scanning a subtree — that's the difference between "fast at 50 items" and "fast at 5,000."This is the zoneless change detection lesson's "check exactly what changed, nothing more" principle applied at two different granularities — which components get checked, and which DOM nodes get touched inside one. It's also directly comparable to React's key prop on lists: both track and key exist to tell a reconciler which rendered node maps to which data item across a re-render, so the framework can patch instead of rebuild.