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:
- Check every event: Angular triggers a full change detection cycle for each event that occurs, including user interactions like clicks and keyboard events. It also triggers for asynchronous operations like
setInterval
andPromise
completions, thanks to the usage of the zone.js library. - Check every component: Angular checks every component in the component tree (from root to leaf), regardless of whether they are affected by the change.
That is why this “just works”:
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:
- Reduced performance: Frequent and unnecessary checks slow down the application, making it less responsive.
- Increased CPU usage: Each check consumes CPU resources, and with many components, this adds up.
- Poor scalability: As the application grows, the performance degradation becomes more noticeable, making it harder to maintain a smooth user experience.
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:
- An input property of the component changes (i.e.
@Input()
orinput()
). - A DOM event is emitted from the component (including
@Output()
oroutput()
). - The component, or one of its descendants, explicitly calls
markForCheck()
ordetectChanges()
on theChangeDetectorRef
.
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:
- Optimized performance: By limiting change detection to specific scenarios,
OnPush
reduces the number of checks and the overall CPU usage. - Predictable change detection: With
OnPush
, you have more control over when and how changes are detected, making it easier to manage and debug. - 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
:
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:
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:
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:
markForCheck()
: Marks the component and its ancestors for check, scheduling a change detection run for the next cycle.detectChanges()
: Triggers change detection for the component and its children immediately. Note that this can be more performance-intensive if used improperly.
For example, using the ChangeDetectorRef
to trigger change detection:
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:
- The
Default
strategy is convenient at first, but may lead to unnecessary checks and degraded performance in large applications. - The
OnPush
strategy, by contrast, offers a more efficient and granular way to manage change detection, leading to improved performance and scalability.
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:
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:
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.
- Angular’s Change Detection
- Default: A Catch-All Approach
- Performance Issues with the Default Strategy
- OnPush: Efficient and Granular
- Performance Improvements using OnPush
- Adopting OnPush in Your Code
- Use the OnPush strategy
- Refactor Using Signals
- Refactor Using the Async Pipe
- Refactor Using ChangeDetectorRef
- Conclusion: use OnPush
- Bonus 1: Automate Using Schematics
- Bonus 2: Automate Using ESLint