Skip to main content

Angular OnPush Change Detection Strategy

Introduction

Angular's change detection mechanism is responsible for keeping the UI synchronized with the application state. By default, Angular uses a default change detection strategy which checks every component in the application whenever any event occurs (clicks, HTTP responses, timers, etc.). While this ensures the UI is always up-to-date, it can lead to performance issues in large applications.

The OnPush change detection strategy provides a way to optimize your Angular applications by skipping unnecessary change detection cycles. This tutorial will guide you through understanding, implementing, and mastering this performance optimization technique.

Understanding Change Detection in Angular

Before diving into OnPush, let's understand how change detection works in Angular:

Default Strategy

With the default strategy, Angular checks every component in your application's component tree whenever:

  • Any DOM event occurs (click, submit, etc.)
  • An HTTP request completes
  • A timer (setTimeout, setInterval) fires

This ensures your UI is always updated but might lead to many unnecessary checks in complex applications.

What is the OnPush Strategy?

The OnPush change detection strategy tells Angular to only check a component when:

  1. An @Input() property reference changes (not just its internal properties)
  2. An event originating from the component or its children occurs
  3. Change detection is manually triggered
  4. Async pipe in the template emits a new value

This significantly reduces the number of checks Angular needs to perform, improving performance.

Implementing OnPush Change Detection

Basic Implementation

To use OnPush change detection, modify your component decorator:

typescript
import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
// component code
}

Example: Component with OnPush

Let's create a simple user card component:

typescript
// user-card.component.ts
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
import { User } from '../models/user.model';

@Component({
selector: 'app-user-card',
template: `
<div class="card">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<p>Last updated: {{ currentTime }}</p>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
@Input() user: User;
currentTime = new Date().toLocaleTimeString();

constructor() {
// This will update every second, but won't trigger change detection
setInterval(() => {
this.currentTime = new Date().toLocaleTimeString();
console.log('Time updated to', this.currentTime);
}, 1000);
}
}
typescript
// parent.component.ts
import { Component } from '@angular/core';

@Component({
selector: 'app-parent',
template: `
<button (click)="updateUser()">Update User</button>
<app-user-card [user]="user"></app-user-card>
`
})
export class ParentComponent {
user = { name: 'John Doe', email: '[email protected]' };

updateUser() {
// This won't trigger change detection in the child component
this.user.name = 'Jane Doe';

// This will trigger change detection because reference changes
// this.user = { ...this.user, name: 'Jane Doe' };
}
}

What happens in this example:

  • The currentTime property updates every second in the JavaScript
  • However, the time displayed in the template won't update because OnPush prevents change detection
  • When clicking "Update User", modifying properties of the user object won't trigger change detection
  • Uncommenting the second approach would create a new object reference, which would trigger change detection

Working with OnPush Strategy

Let's explore how to effectively work with OnPush strategy:

1. Immutable Data Patterns

When using OnPush, you should treat your data as immutable:

typescript
// Instead of this (doesn't work well with OnPush):
updateUser() {
this.user.name = 'Jane Doe';
}

// Do this instead (works with OnPush):
updateUser() {
this.user = { ...this.user, name: 'Jane Doe' };
}

2. Manual Change Detection

Sometimes you need to manually trigger change detection:

typescript
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';

@Component({
selector: 'app-counter',
template: `<p>Count: {{ count }}</p>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;

constructor(private cdr: ChangeDetectorRef) {
setInterval(() => {
this.count++;
// Manually mark for check
this.cdr.markForCheck();
}, 1000);
}
}

The ChangeDetectorRef provides several methods:

  • markForCheck(): Marks the component and its ancestors for checking
  • detectChanges(): Runs change detection on the component and its children
  • detach(): Detaches the change detector
  • reattach(): Reattaches the change detector

3. Using Observables with Async Pipe

The async pipe works excellently with OnPush strategy because it automatically triggers change detection when new values arrive:

typescript
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { interval } from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
selector: 'app-timer',
template: `<p>Time: {{ time$ | async }}</p>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimerComponent {
time$ = interval(1000).pipe(
map(() => new Date().toLocaleTimeString())
);
}

Real-World Example: Data Table with OnPush

Let's see a more practical example of a data table component using OnPush:

typescript
// data-table.component.ts
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';

interface TableData {
items: any[];
totalCount: number;
}

@Component({
selector: 'app-data-table',
template: `
<table>
<thead>
<tr>
<th *ngFor="let header of headers">{{ header }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of data.items">
<td *ngFor="let header of headers">{{ item[header.toLowerCase()] }}</td>
</tr>
</tbody>
<tfoot>
<tr>
<td [colSpan]="headers.length">
Total Items: {{ data.totalCount }}
</td>
</tr>
</tfoot>
</table>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DataTableComponent {
@Input() headers: string[] = [];
@Input() data: TableData = { items: [], totalCount: 0 };
}
typescript
// user-list.component.ts
import { Component } from '@angular/core';
import { UserService } from '../services/user.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
selector: 'app-user-list',
template: `
<div>
<button (click)="sortByName()">Sort by Name</button>
<app-data-table
[headers]="tableHeaders"
[data]="tableData$ | async">
</app-data-table>
</div>
`
})
export class UserListComponent {
tableHeaders = ['Name', 'Email', 'Role'];
tableData$: Observable<any>;

constructor(private userService: UserService) {
this.tableData$ = this.userService.getUsers().pipe(
map(users => ({
items: users,
totalCount: users.length
}))
);
}

sortByName() {
// This will work with OnPush since we're creating a new stream
this.tableData$ = this.userService.getUsers().pipe(
map(users => users.sort((a, b) => a.name.localeCompare(b.name))),
map(sortedUsers => ({
items: sortedUsers,
totalCount: sortedUsers.length
}))
);
}
}

In this example:

  1. The DataTableComponent uses OnPush change detection
  2. The parent component passes data using the async pipe
  3. When sorting, we create a new observable rather than mutating existing data

Common Pitfalls with OnPush

1. Mutating Input Objects

This won't trigger change detection with OnPush:

typescript
// Parent component
updateUser() {
// ❌ Won't trigger OnPush change detection
this.user.name = 'Jane';
}

Solution:

typescript
// Parent component
updateUser() {
// ✅ Will trigger OnPush change detection
this.user = { ...this.user, name: 'Jane' };
}

2. Objects in Template Expressions

Be careful with inline objects in templates:

html
<!-- ❌ Creates a new object on every check -->
<app-user-card [style]="{color: 'red'}"></app-user-card>

Solution:

typescript
// Component class
redStyle = {color: 'red'};
html
<!-- ✅ Uses reference that doesn't change -->
<app-user-card [style]="redStyle"></app-user-card>

3. Forgetting to Handle Async Operations

Always use proper techniques for async operations:

typescript
// ❌ Won't update UI with OnPush
fetchData() {
this.http.get('/api/data').subscribe(data => {
this.data = data;
});
}

Solutions:

typescript
// ✅ Option 1: Use async pipe
data$ = this.http.get('/api/data');
html
<div *ngIf="data$ | async as data">...</div>
typescript
// ✅ Option 2: Manual change detection
constructor(private cdr: ChangeDetectorRef) {}

fetchData() {
this.http.get('/api/data').subscribe(data => {
this.data = data;
this.cdr.markForCheck();
});
}

Performance Benefits

Using OnPush strategy can significantly improve performance in large applications by:

  1. Reducing the number of components checked during change detection
  2. Preventing unnecessary re-rendering
  3. Forcing developers to use more predictable data flow patterns

An application with hundreds of components might see significant performance improvements, especially on less powerful devices.

When to Use OnPush

Consider using OnPush strategy when:

  • You have a large application with many components
  • Components rarely need updates
  • You're working with immutable data structures
  • Your components receive inputs that don't change often
  • You want to enforce a more predictable data flow

Summary

The Angular OnPush change detection strategy is a powerful tool for optimizing your application's performance. By telling Angular to only check components when specific conditions are met, you can significantly reduce the change detection workload.

Key points to remember:

  • OnPush only triggers change detection when input references change, events occur, or when manually triggered
  • Use immutable data patterns (create new objects instead of mutating existing ones)
  • The async pipe works great with OnPush as it automatically triggers change detection
  • Use ChangeDetectorRef methods when you need manual control

By applying these techniques, you'll create more efficient Angular applications that provide better user experiences, especially in large-scale applications.

Additional Resources

Exercises

  1. Create a simple counter component using OnPush and implement three different ways to update the counter (event-based, input-based, and manually triggering change detection).

  2. Refactor an existing component in your application to use OnPush strategy and observe the performance differences.

  3. Build a parent-child component relationship where the parent passes data to multiple children, using OnPush in the children components. Test different ways of updating the data and see how it affects change detection.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)