Explain OnPush Change Detection in Angular Like You’re Debugging a Real Production Bug

LowEasyAngular
Preparing for interviews?

Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.

Quick Answer

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.

Answer

Core idea

ChangeDetectionStrategy.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.

What triggers change detection in OnPush components?

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.

TYPESCRIPT
@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

markForCheck()

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).

detectChanges()

Immediately runs change detection on this component subtree.

Rare cases: manual rendering, detached views, or very controlled performance scenarios.

Manual change detection tools

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 async pipe

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 markForCheck()

Otherwise Angular will not re-render.

Correct patterns with OnPush

Interview-level summary

OnPush 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.

Similar questions
Guides
16 / 37