How to use Angular OnPush Change Detection
One of the most common suggestions for improving performance in Angular is using the OnPush change detection strategy. While it does improve performance, it can lead to unwanted behavior if you're not aware of how it works.
One of the most common suggestions for improving performance in Angular is using the OnPush change detection strategy. While it does improve performance, it can lead to unwanted behavior if you're not aware of how it works.
In this post, I'll try to clear up how the OnPush change detection strategy works, and how you can avoid some of its common pitfalls.
What is change detection?
Angular uses change detection to know when the model representing the component has changed.
The values of the templated bindings are compared with the values in the previous iteration of change detection. When a change is detected, the template is re-rendered.
This is done to ensure that what the user is seeing is up-to-date with the data in the app.
When does change detection happen?
Change detection happens whenever an async event triggers because this is when the model is likely to change. These can be:
- Browser events (like clicking a button or typing in an input)
- HTTP requests (using XmlHttpRequest)
- Timers (setTimeout and setInterval)
Angular uses something called _zones_ to detect when these async events are finished, and then triggers the change detection. I won't get into much more detail on zones, but if you're interested in diving deeper, I recommend reading "How Angular uses NgZone/Zone.js for Dirty Checking".
Whenever the zone alerts Angular that the async events are done, Angular starts detecting changes from the root of the component tree.
From there, Angular will detect changes down the component tree based on the change detection strategy.
What is a change detection strategy?
A change detection strategy is the way Angular determines when it has to detect changes in the component, based on the changes to its data model.
This strategy can be set inside the component decorator, and there are two values it can take: Default and OnPush.
@Component({
selector: 'hello',
templateUrl: './hello.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HelloComponent { ... }
Default change detection strategy
If no value is specified, a component will use the Default change detection strategy.
In the Default strategy, all components will be checked whenever the change detection runs. As your application grows, the number of change detections will increase a lot. This is why using the OnPush change detection strategy can help with performance.
In fact, this is the reason why you should avoid using functions in your template. Whenever change detection runs, it has to compare the values of the bindings. To get the value of function bindings, it needs to evaluate them every time change detection runs.
OnPush change detection strategy
The main difference with the OnPush strategy is that change detection only runs when the input values (the variables with the @Input decorator) change. If the input values are the same, that component and its subtree will not be checked for changes.
So what constitutes a change to the input? The input reference itself has to change. This means that if you have an object, simply changing a property of the input object won't work. You need to change the actual object too.
// Won't trigger change detection 👎
inputObj.name = "Jane";
// Triggers change detection because the object reference changes 👍
inputObj = { ...inputObj, name: "Jane" };
Now, what would happen if a user clicks a button inside a component down the tree, but one of its ancestors doesn't have @Input changes? That component probably changed, but it won't be checked, right?
Well, Angular is smart about this. It labels the component to make sure that it will be checked, meaning the whole path to that component in the tree will be checked.
This means that a component is checked if one of its @Input variables changes, or if the async event triggering the change detection happened inside the component.
How to use the OnPush strategy effectively
Knowing the essentials of OnPush allows us to use it more effectively, avoiding the issues that are often associated with it. Here are the main things to keep in mind when developing using the OnPush change detection strategy.
@Input
When using @Input properties, make sure that the changes to these values are immutable.
This can be done through best practices or, more safely, using an immutability library.
// 👎 Bad
inputObj.name = "Jane";
// 👍 Good
inputObj = { ...inputObj, name: "Jane" };
Observables
Observables are one of the main ways to implement asynchronous behavior in Angular.
If they are asynchronous, they should trigger the change detection, right? Not exactly.
If you subscribe directly to an observable, Angular won't listen to it.
If you want the observable values to trigger change detection, you have to use them directly in the component template, with the async pipe.
/* Direct observable subscription */
// *.component.html
<p>{{ name }}</p>
/* This won't trigger change detection because Angular doesn't listen to the subscription inside the component */
// *.component.ts
nameObservable.subscribe( x => {
this.name = x;
})
/* Observable subscription through async pipe */
// *.component.html
<p>{{ nameObservable | async }}</p>
Triggering change detection manually
If you can't follow these recommendations, you can trigger change detections manually.
There are two ways to do it, using a ChangeDetectionRef which can be injected into a component:
- Using `detectChanges()`, the component will be checked for changes as well as its subtree.
nameObservable.subscribe((x) => {
this.name = x;
// This will immediately detect the changes in the template and update it to match the view
this.cdRef.detectChanges();
});
- Using `markForCheck()`, the component will be marked for checking whenever the next iteration of change detection runs. Instead of immediately checking the changes, Angular will wait until it runs the change detection.
nameObservable.subscribe((x) => {
this.name = x;
// This is equivalent to the change detection triggered by the async pipe
this.cdRef.markForCheck();
});
Playground
I setup a quick playground where you can play around with these principles.
https://stackblitz.com/edit/angular-onpush-playground?embed=1&file=src/app/app.component.html
Conclusion
As we've seen throughout this post, the OnPush change detection strategy can be a good way to improve the performance of your Angular apps.
This will introduce some caveats into the development process, but, if you take some precautions, you can easily introduce it to your apps. These include:
- Using immutable updates to @Input properties
- Using the async pipe to subscribe to Observables
- Implementing a custom change detection strategy using the functions
detectChanges
andmarkForCheck
I hope this was a useful introduction to the OnPush strategy.
If you have any questions or suggestions, you can reach me on Twitter @pedronavelopes.
Thanks for reading and have a nice day!