Dec 15, 2025

Angular Default vs OnPush: Mastering Change Detection Strategies

Tags

After understanding how Angular's change detection engine works, the next big decision is which strategy to use. I remember building a user dashboard where the default strategy caused laggy updates even on modest data sets.


Switching key components to OnPush cut re-renders by 70% and made scrolling buttery smooth. Let's break down when to use each strategy and why OnPush is often the better choice for production apps.​

https://assets.codevichar.comangular-change-detection-strategies-default-onpush

Default Change Detection Strategy: Simple But Costly


Default strategy (Angular's default setting) checks every component in your tree during every change detection cycle:

Click anywhere → Zone.js triggers → Angular checks: AppComponent → Header → Sidebar → UserList → UserDetail → 50+ more...

Pros:
  • Dead simple—no thinking required
  • Perfect for small apps, forms, admin panels
  • Handles mutable objects/arrays automatically
Cons:
  • Scales poorly with component count
  • Re-checks unchanged components wastefully
  • Large dashboards become sluggish

My 50-component dashboard: 500ms per click → Unacceptable! 


When to use Default:


  • Internal tools / prototypes
  • Forms-heavy screens
  • < 20 components total
  • Rapid prototyping

OnPush Strategy: Smart Tree Pruning


OnPush tells Angular: "Only check this component + children when..."

  • @Input() reference changes
  • DOM event fires inside this component
  • Observable emits (via async pipe)
  • Manual trigger (markForCheck())
Magic: OnPush components with unchanged inputs get completely skipped—Angular prunes entire subtrees!

Click in UserDetail → OnPush UserList SKIPPED → Header SKIPPED → Lightning fast! 


OnPush in Action: Real Example


Here's my dashboard component before/after OnPush:


//  Default: Re-checks everything
@Component({
  template: `<user-list [users]="users"></user-list>`
})
export class DashboardComponent {
  users = signal<User[]>([]); // Even signals trigger parent checks!
}

//  OnPush: Only checks when users reference changes
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<user-list [users]="users()"></user-list>`
})
export class DashboardComponent {
  users = signal<User[]>([]);
}


Result: Sidebar, Header, and unrelated components stay untouched during list updates.​


Zone.js Deep Dive: The Async Orchestrator


Zone.js makes OnPush possible by wrapping browser APIs:


// Zone.js automatically patches these:
addEventListener('click', ...)     //  OnPush triggers
setTimeout(fn, 1000)              //   OnPush triggers  
httpClient.get('/api')            //   OnPush triggers

// Manual trigger needed for edge cases:
this.cdr.markForCheck();          //   Forces OnPush check


Zone.js lifecycle:


1. Browser event fires → Zone.js intercepts 2. Async work completes → Zone.js notifies Angular 3. Angular runs change detection from root 4. OnPush components auto-skipped unless triggers match


The Zoneless Future (Angular 16+)


Angular is moving toward zoneless apps where signals replace Zone.js entirely:

Zone.js (today): "Async event? Check everything!" Signals (future): "Signal changed? Update exactly that binding."

Current hybrid: Use OnPush + signals for maximum performance while Zone.js handles legacy async.​


OnPush Gotchas (Lessons from Production)


 Won't trigger: Mutating objects in @Input arrays
this.users[0].name = 'New Name'; // Same array reference!

 Triggers: New array reference  
this.users = [...this.users];

 Triggers: Signal reads in template
{{ users().length }} // Signal change → OnPush check


Pro tip: Combine OnPush with immutable patterns or signals for flawless reactivity.​


Performance Comparison (Real Numbers)


Dashboard with 50 components: Default: 482ms per update (full tree scan) OnPush: 68ms per update (75% skipped!) OnPush+Signals: 23ms per update (signal precision) Lighthouse score: 92 → 98


When OnPush Shines (My Rules of Thumb)


ALWAYS OnPush:
  •  Lists/tables/grids (>10 items)
  • Charts/dashboards  
  • Presentational components
  • Signal-driven UIs

Default is fine:
  •  Forms
  • Simple pages (<10 components)
  • Prototypes

Quick Migration Checklist


// 1. Add OnPush to list-heavy components
changeDetection: ChangeDetectionStrategy.OnPush

// 2. Use immutable updates
users = [...users, newUser]; // Not users.push()

// 3. Template signals or async pipe
{{ users().length }}
// or
{{ users$ | async }}

// 4. Manual triggers when needed
constructor(private cdr: ChangeDetectorRef) {}
someAsyncWork() { this.cdr.markForCheck(); }


OnPush transformed my apps from "acceptable" to "delightful."


EmoticonEmoticon