How to use OnPush change detection in Angular
OnPush change detection in Angular dramatically improves performance by skipping change detection when input references don’t change. As the creator of CoreUI with over 12 years of Angular experience since 2014, I’ve used OnPush to optimize performance in enterprise dashboards. OnPush strategy runs change detection only when input properties change reference, events fire, or Observables emit new values. This approach reduces unnecessary checks and makes applications faster, especially with large component trees.
Use ChangeDetectionStrategy.OnPush to optimize Angular performance by limiting when change detection runs.
Basic OnPush configuration:
// user-list.component.ts
import { Component, Input, ChangeDetectionStrategy } from '@angular/core'
@Component({
selector: 'app-user-list',
template: `
<div>
<h2>Users</h2>
<div *ngFor="let user of users">
{{ user.name }} - {{ user.email }}
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent {
@Input() users: User[] = []
}
Immutable data updates:
// parent.component.ts
import { Component } from '@angular/core'
@Component({
selector: 'app-parent',
template: `
<button (click)="addUser()">Add User</button>
<app-user-list [users]="users"></app-user-list>
`
})
export class ParentComponent {
users = [
{ id: 1, name: 'John', email: '[email protected]' },
{ id: 2, name: 'Jane', email: '[email protected]' }
]
// WRONG - mutation doesn't trigger OnPush
addUserWrong() {
this.users.push({ id: 3, name: 'Bob', email: '[email protected]' })
}
// CORRECT - new reference triggers OnPush
addUser() {
this.users = [...this.users, { id: 3, name: 'Bob', email: '[email protected]' }]
}
// CORRECT - updating single user
updateUser(id: number, name: string) {
this.users = this.users.map(user =>
user.id === id ? { ...user, name } : user
)
}
// CORRECT - removing user
removeUser(id: number) {
this.users = this.users.filter(user => user.id !== id)
}
}
OnPush with Observables:
// data.component.ts
import { Component, ChangeDetectionStrategy } from '@angular/core'
import { Observable } from 'rxjs'
import { DataService } from './data.service'
@Component({
selector: 'app-data',
template: `
<div *ngIf="data$ | async as data">
<h2>{{ data.title }}</h2>
<p>{{ data.content }}</p>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DataComponent {
data$: Observable<any>
constructor(private dataService: DataService) {
this.data$ = this.dataService.getData()
}
}
Manual change detection with OnPush:
// counter.component.ts
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'
@Component({
selector: 'app-counter',
template: `
<div>
<p>Count: {{ count }}</p>
<button (click)="increment()">Increment</button>
<button (click)="startTimer()">Start Timer</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0
constructor(private cdr: ChangeDetectorRef) {}
// Event handlers automatically trigger change detection
increment() {
this.count++
}
// External updates need manual trigger
startTimer() {
setInterval(() => {
this.count++
// Manually mark for check
this.cdr.markForCheck()
}, 1000)
}
}
OnPush with EventEmitter:
// child.component.ts
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'
@Component({
selector: 'app-child',
template: `
<div>
<p>{{ message }}</p>
<button (click)="notify()">Notify</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
@Input() message: string
@Output() messageChanged = new EventEmitter<string>()
notify() {
// Events trigger change detection in parent
this.messageChanged.emit('Message from child')
}
}
OnPush with complex objects:
// user-profile.component.ts
import { Component, Input, ChangeDetectionStrategy } from '@angular/core'
interface User {
id: number
name: string
profile: {
age: number
city: string
}
}
@Component({
selector: 'app-user-profile',
template: `
<div>
<h3>{{ user.name }}</h3>
<p>Age: {{ user.profile.age }}</p>
<p>City: {{ user.profile.city }}</p>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserProfileComponent {
@Input() user: User
}
// Parent component - correct updates
@Component({
template: `<app-user-profile [user]="user"></app-user-profile>`
})
export class ParentComponent {
user: User = {
id: 1,
name: 'John',
profile: { age: 30, city: 'New York' }
}
// WRONG - nested mutation
updateCityWrong() {
this.user.profile.city = 'Boston'
}
// CORRECT - new object reference
updateCity() {
this.user = {
...this.user,
profile: {
...this.user.profile,
city: 'Boston'
}
}
}
}
OnPush with services:
// notification.service.ts
import { Injectable } from '@angular/core'
import { BehaviorSubject } from 'rxjs'
@Injectable({
providedIn: 'root'
})
export class NotificationService {
private notificationSubject = new BehaviorSubject<string>('')
notification$ = this.notificationSubject.asObservable()
showNotification(message: string) {
this.notificationSubject.next(message)
}
}
// notification.component.ts
import { Component, ChangeDetectionStrategy } from '@angular/core'
import { NotificationService } from './notification.service'
@Component({
selector: 'app-notification',
template: `
<div *ngIf="notification$ | async as message" class="notification">
{{ message }}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NotificationComponent {
notification$ = this.notificationService.notification$
constructor(private notificationService: NotificationService) {}
}
OnPush with trackBy:
// list.component.ts
import { Component, Input, ChangeDetectionStrategy } from '@angular/core'
@Component({
selector: 'app-list',
template: `
<ul>
<li *ngFor="let item of items; trackBy: trackById">
{{ item.name }}
</li>
</ul>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent {
@Input() items: any[]
trackById(index: number, item: any) {
return item.id
}
}
Detach and reattach:
// performance.component.ts
import { Component, ChangeDetectorRef, ChangeDetectionStrategy, OnDestroy } from '@angular/core'
@Component({
selector: 'app-performance',
template: '<div>{{ data }}</div>',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PerformanceComponent implements OnDestroy {
data: string
constructor(private cdr: ChangeDetectorRef) {}
startHeavyOperation() {
// Detach from change detection during heavy operation
this.cdr.detach()
// Perform operation
this.processData()
// Reattach and update
this.cdr.reattach()
this.cdr.markForCheck()
}
processData() {
// Heavy computation
this.data = 'Processed'
}
ngOnDestroy() {
// Clean up
this.cdr.detach()
}
}
Best Practice Note
Use OnPush for leaf components that display data passed via inputs. Always create new object references when updating—use spread operator or Array methods like map, filter. Async pipe automatically triggers change detection—perfect for OnPush components. Call markForCheck() when updating from external sources like timers or WebSocket callbacks. Combine OnPush with trackBy for optimal ngFor performance. Use immutable update patterns consistently across your application. This is how we optimize CoreUI Angular applications—OnPush change detection throughout component trees, immutable data updates, and strategic use of ChangeDetectorRef for maximum performance.



