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:
- An
@Input()
property reference changes (not just its internal properties) - An event originating from the component or its children occurs
- Change detection is manually triggered
- 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:
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:
// 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);
}
}
// 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:
// 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:
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 checkingdetectChanges()
: Runs change detection on the component and its childrendetach()
: Detaches the change detectorreattach()
: 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:
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:
// 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 };
}
// 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:
- The
DataTableComponent
uses OnPush change detection - The parent component passes data using the
async
pipe - 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:
// Parent component
updateUser() {
// ❌ Won't trigger OnPush change detection
this.user.name = 'Jane';
}
Solution:
// 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:
<!-- ❌ Creates a new object on every check -->
<app-user-card [style]="{color: 'red'}"></app-user-card>
Solution:
// Component class
redStyle = {color: 'red'};
<!-- ✅ 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:
// ❌ Won't update UI with OnPush
fetchData() {
this.http.get('/api/data').subscribe(data => {
this.data = data;
});
}
Solutions:
// ✅ Option 1: Use async pipe
data$ = this.http.get('/api/data');
<div *ngIf="data$ | async as data">...</div>
// ✅ 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:
- Reducing the number of components checked during change detection
- Preventing unnecessary re-rendering
- 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
- Angular Documentation on Change Detection
- Angular University: Angular OnPush Change Detection
- Netanel Basal: Understanding Change Detection in Angular
Exercises
-
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).
-
Refactor an existing component in your application to use OnPush strategy and observe the performance differences.
-
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! :)