/* */

Angular Change Detection: Why You Should Always Use OnPush

TLDR: By implementing the OnPush strategy along with modern reactive patterns, you can make your code more efficient, predictable, and scalable. It also makes your code more reactive, preparing your code for the zone-less future of Angular.


Change detection is the mechanism by which Angular updates the view in response to changes in the model. If you’re working with Angular, understanding change detection and its limitations is crucial to create performant web applications.

Angular provides two change detection strategies: the Default and the OnPush strategy. In this article, we’ll dive deep into both strategies and why you should use the OnPush change detection strategy in your projects.

Angular’s Change Detection

Change detection is the process through which Angular updates the view whenever the application state changes. There are two main strategies for change detection in Angular: the Default strategy and the OnPush strategy.

Default: A Catch-All Approach

The Default change detection strategy ensures that all potential changes are captured and reflected in the view. This approach is known as the “check everything” strategy:

That is why this “just works”:

@Component({
template: `{{ counter }} seconds passed.`,
})
export class CounterComponent implements OnInit {
counter = 0;
ngOnInit() {
setInterval(() => {
this.counter++;
}, 1000);
}
}

Each time the interval triggers (every second), a full change detection cycle is run by Angular.

Performance Issues with the Default Strategy

In default mode, every event triggers a full change detection cycle, checking all components from the root to the leaf. As you can imagine, this can lead to significant performance issues in large-scale applications.

Consider a data table component displaying a large dataset. Each row is a separate component, containing UI elements like buttons and badges. Using the default change detection strategy, any interaction (e.g. sorting, filtering, or even simple clicks) will trigger change detection for every component in the component tree. This significantly hurts performance.

Using the Default strategy can soon lead to:

OnPush: Efficient and Granular

The OnPush change detection strategy, on the other hand, optimizes this process by opting out of this “check everything” pattern. It offers a more granular way of telling Angular when and what to update.

Components that use the OnPush strategy limit change detection to itself and its children, and will run change detection only when:

This means that even if a component property is updated internally, the view won’t update unless an input changes or change detection is manually triggered.

Performance Improvements using OnPush

Using OnPush can lead to significant performance improvements, especially in large applications with large component trees.

Using the OnPush strategy can lead to:

  1. Optimized performance: By limiting change detection to specific scenarios, OnPush reduces the number of checks and the overall CPU usage.
  2. Predictable change detection: With OnPush, you have more control over when and how changes are detected, making it easier to manage and debug.
  3. Improved scalability: Applications using OnPush can handle larger component trees more efficiently, leading to better scalability.

Adopting OnPush in Your Code

Most common patterns that rely on the Default strategy can be refactored easily to use the OnPush strategy. Instead of leaning on the automatic change detection of Angular, you could use signals, observables and the async pipe, or trigger change detection manually.

Use the OnPush strategy

To make your component adopt the OnPush strategy, you need to set the changeDetection property of the component’s decorator to ChangeDetectionStrategy.OnPush:

import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterComponent {}

With this simple change, your component is now more performant and will only re-render in certain cases.

Refactor Using Signals

Angular is adopting signals as the future for reactivity. Signals are reactive primitives that represent values over time, automatically tracking dependencies and updating any affected parts of the application when those values change.

Instead of static class properties, you can use a signal to make your counter value reactive:

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `{{ counter() }} seconds passed.`,
})
export class CounterComponent {
readonly counter = signal(0);
constructor() {
setInterval(() => {
// Update the value based on the current value:
this.counter.update((value) => value + 1);
// Alternatively, using the set method:
this.counter.set(this.counter() + 1);
}, 1000);
}
}

Refactor Using the Async Pipe

You can use the async pipe to subscribe to an observable in your component.

Every time the value of this observable updates, a change detection cycle is triggered. Using the async pipe also prevents you from having to subscribe and unsubscribe manually.

Here is the counter example, updated to use the async pipe:

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `{{ counter | async }} seconds passed.`,
})
export class CounterComponent {
readonly counter = new BehaviorSubject(0);
constructor() {
setInterval(() => {
this.counter.next(this.counter.value + 1);
}, 1000);
}
}

Refactor Using ChangeDetectorRef

Sometimes you may need to trigger change detection manually. Angular provides the ChangeDetectorRef service for this purpose.

The service exposes two methods:

For example, using the ChangeDetectorRef to trigger change detection:

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `{{ counter }} seconds passed.`,
})
export class CounterComponent {
counter = 0;
#cdr = inject(ChangeDetectorRef);
constructor() {
setInterval(() => {
this.counter++;
// Mark this component for check:
this.#cdr.markForCheck();
// Or, check immediately:
this.#cdr.detectChanges();
}, 1000);
}
}

Conclusion: use OnPush

Angular’s change detection is a powerful feature. However, especially in large applications, it can also be a performance bottleneck if not used wisely.

Angular offers two strategies for change detection:

By implementing the OnPush strategy, along with modern reactive patterns, you can make your code more efficient, predictable and scalable. It also makes your code more reactive, preparing your code for the zone-less future of Angular.

Bonus 1: Automate Using Schematics

If you use the Angular CLI, or an IDE extension, to generate Angular components, you can instruct the generator to use the OnPush strategy by default.

In your angular.json file, add the following option:

{
"schematics": {
"@schematics/angular:component": {
// Other options ...
"changeDetection": "OnPush"
}
}
}

Now, when you generate a component, it will automatically implement the OnPush change detection strategy.

Bonus 2: Automate Using ESLint

You can use ESLint to make sure that you create efficient component that implement the OnPush change detection strategy.

Include the following ESLint rule in your Angular project’s eslintrc file:

{
"rules": {
// Other rules ...
"@angular-eslint/prefer-on-push-component-change-detection": 2
}
}

Now, ESLint will throw an error if your component does not use the OnPush strategy, and even gives you the option to automatically update your code to implement this strategy.