OnPush change detection tells Angular to skip checking a component unless one of a few specific triggers happens (Input reference change, event inside the component, async pipe emission, or manual markForCheck/detectChanges). It dramatically improves performance, but breaks the UI if you mutate objects or arrays instead of changing references.
Explain OnPush Change Detection in Angular Like You’re Debugging a Real Production Bug
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Core ideaChangeDetectionStrategy.OnPush tells Angular: “Only check this component if something meaningful happened.” Meaningful = an @Input reference changed, an event happened in this component, an async pipe emitted, or you manually asked Angular to check.
What happened? | Will OnPush re-render? | Why |
|---|---|---|
An @Input reference changed | ✅ Yes | Angular compares references, not deep values. |
A click/event inside the component | ✅ Yes | Events mark the component dirty. |
An async pipe emits a new value | ✅ Yes | AsyncPipe internally calls markForCheck(). |
You mutated an object/array in place | ❌ No | Reference did not change, Angular thinks nothing happened. |
You call markForCheck() manually | ✅ Yes | You explicitly told Angular to re-check. |
You call detectChanges() manually | ✅ Yes (immediate) | Forces synchronous change detection for this subtree. |
The classic real-world bug
“My data changes but the UI doesn’t update.” This almost always means you mutated an object or array instead of creating a new reference.
@Component({
selector: 'app-user-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div *ngFor="let user of users">{{ user.name }}</div>
`
})
export class UserListComponent {
@Input() users: { name: string }[] = [];
// ❌ BUG: UI will NOT update in OnPush
addUserWrong() {
this.users.push({ name: 'New User' }); // same array reference
}
// ✅ Correct: new reference => OnPush sees change
addUserCorrect() {
this.users = [...this.users, { name: 'New User' }];
}
}
Why OnPush exists
Default change detection checks the whole component tree on every async event (timers, HTTP, clicks, etc). OnPush lets Angular skip huge parts of the tree unless something relevant changed. This is critical for large apps and big lists.
markForCheck() vs detectChanges()
Method | What it does | When to use |
|---|---|---|
| Marks this component and its parents as dirty for the next CD cycle. | When data changes outside Angular's awareness (e.g., custom subscriptions, timers, non-async-pipe streams). |
| Immediately runs change detection on this component subtree. | Rare cases: manual rendering, detached views, or very controlled performance scenarios. |
AsyncPipe is your best friend
The async pipe automatically:
• Subscribes
• Unsubscribes
• Calls markForCheck() on emission
This is why OnPush + async pipe is the recommended default pattern.
Common senior-level pitfalls
• Mutating arrays/objects instead of replacing them
• Forgetting that OnPush uses reference checks, not deep checks
• Updating state inside subscriptions without async pipe or markForCheck
• Using OnPush everywhere without understanding its mental model
Scenario | What to do | Why |
|---|---|---|
State comes from Observables | Use | Automatic CD + auto unsubscribe + best performance. |
State updated manually in code | Use immutable updates (new reference) | So OnPush can detect the change. |
State updated outside Angular or in manual subscriptions | Call | Otherwise Angular will not re-render. |
Interview-level summaryOnPush makes Angular check a component only when inputs change by reference, an event happens, an async pipe emits, or you manually trigger detection. It gives big performance wins, but requires immutable state updates. Most “UI not updating” bugs in OnPush come from mutating objects instead of replacing them.