Angular Change Detection

tldr;

Change detection in Angular is a core part of the framework, and understanding when it runs and how to manage it is essential to creating stable and performant applications. This article will cover those topics as well as a few things to look out for in your app to ensure you don’t negatively impact the performance of your app.

Background

If you’ve been around Angular apps for any amount of time, you’re sure to have heard this Angular buzzword (buzzterm?): change detection. If you’re like me, you heard it but basically ignored it for years because it was overwhelming and you didn’t really know where to jump in. This article will hopefully help you understand change detection and make it not seem so big and scary.

So what is change detection? At a high level, change detection is the process of checking to see if something in your Angular application has changed, and then rendering the changes to the DOM so the user can see the updated application. That’s not so bad, right? In the rest of the article we’ll talk more about what change detection is, how it’s implemented in Angular, and some pitfalls to avoid.

Change Detection

Change detection in Angular is implemented using Zone.js. Zone.js is a library that essentially patches many lower level browser APIs (like event handlers) so that the default functionality of the event occurs, as well as some custom Angular functionality. Zone.js patches all of the browser events (like click events, mouse events, etc), setTimeout and setInterval, and Ajax HTTP requests. If any of these events occur in your Angular app, Zone.js will cause change detection to run.

Angular starts at the bottom of the application’s component tree to check for changes, and then moves up through the app. When the app finds a component that has changed and needs to be updated, Angular performs a top-down review of the app to see if any of the parent components need to be updated as well. The marking of components as changed happens from bottom-up in the application; the re-rendering happens top-down.

Default Implementation

Each component has a change detector that determines if anything has changed in the component since the last time change detection ran. It looks at the information in the component (variables, template, etc) and compares the current value with the previous value. If the two values are different, they are marked as changed. Angular is also smart enough to only check for changes on values used in a specific component. This means that if you have an attribute on an object that is not used in a component, that component will not check to see if the value of that attribute has changed.

In this change detection method, Angular will check each component on every change detection cycle to see if something has changed and needs updating. This is also referred to as dirty checking. The downside to this is that when you have a large application where performance matters, many components will be checked for changes even though ultimately nothing changed in that component.

OnPush

The other change detection method you can use in Angular applications is called the OnPush method. With this change detection method, the component will only be checked for changes in certain situations. Those situations are:

An input reference changesAn event is emitted from the component or one of its childrenThe developer explicitly marks the component as needing to be checkedThe async pipe is used in the view, or an Observable emits an event

If none of the above conditions are met, then Angular will skip that component on its change detection cycle.

Let’s look at an example of when change detection will run and when it will not run.

this.items = [
{ id: 1, title: ‘First item’ }
]// Example 1
// This will not run change detection in a component
// This means that the title on the screen will remain “First item”
this.items[0].title = ‘First item updated’;// Example 2
// This will run change detection, as the reference changed for this.items
const [first, …rest] = this.items;
first.title = ‘First item updated’;
this.items = [first, …rest];

The above code snippet has two examples. One shows when OnPush change detection will not run, and one shows when it will. The first example will not cause change detection to update the component because the array is being edited in place. The reference to the array of items remains the same.

In the second example, a new array is created and this.items is updated to point to a new location in memory. Change detection will run and the change will be showed on the screen.

Change detection will also run if a button in the component is clicked, or some other event is emitted, as well as if the event occurs in a child component for this component.

Potential Pitfalls

There are certain things you can do in your application that will negatively impact performance and are related to change detection. By avoiding these pitfalls, your app will be more performant. Let’s look at an example.

A common thing that many developers do (I’ve done it also) is call a component class method from the template for the purpose of displaying data or affecting the display of the template. Here’s an example of what I mean:

export class MyComponent {
public preston$: Observable<Person> = of({
firstName: ‘Preston’,
lastName: ‘Lamb’,
}); getFullName(person: Person) {
return person.firstName + ‘ ‘ + person.lastName
}
}

Our component has a variable called preston$ of type Observable<Person>. Use your imagination to pretend that this object is coming from a backend API, and not just declared right in the component. The object has a first and last name attribute, but there’s no full name attribute. We have a method there to combine the first and last names for us so the full name can be output in the template like this:

<p *ngIf=”preston$ | async as preston”>
{{ getFullName(preston) }}
</p>This is a pretty contrived example, because it’s easy enough to just output the first and last names in the template, but it demonstrates when you might call a function from the template.

I’ve done this before, and it doesn’t seem like a big deal. The problem is, though, that Angular doesn’t know if the return value of getFullName has changed or not on each change detection cycle. Because it can’t know for sure, it has to check on every cycle to make sure the value is unchanged and that the correct information is showing. If change detection runs 100 times in your application, the getFullName method will be called 100 times, even if firstName and lastName are never changed. Now you can see where this can impact performance of your app, right? In this case maybe the performance hit is minimal, since two strings are just being concatenated. But if the method that is being called is more complicated and intensive, the negative effect could be more noticeable.

Now that we see the problem, let’s look at how to solve it. Instead of calling the function from the template, let’s add a fullName attribute to the object in the .ts file and calculate the value when we access the data.

interface ExtendedPerson extends Person {
fullName: string;
}export class MyComponent {
public preston$: Observable<ExtendedPerson> = of({
firstName: ‘Preston’,
lastName: ‘Lamb’,
}).pipe(
map((person: Person) => {
return {
…person,
fullName: this.getFullName(person)
}
})
)

getFullName(person: Person) {
return person.firstName + ‘ ‘ + person.lastName
}
}<p *ngIf=”preston$ | async as preston”>
{{ preston.fullName }}
</p>

Instead of calling the method from the template, we added a step in the observable pipeline to calculate the fullName and add it to the object. Now the method is called only when the observable emits an event. Until then, change detection will skip over this component and not run the method each cycle.

There are multiple ways to do accomplish this, but essentially they all come down to adding the attribute to the object before getting to the template so the new attribute can be output.

Creating and using impure pipes are another way to negatively impact the performance of your application. We won’t go into detail in this article on impure pipes, but here’s a brief overview. Pipes by default are pure, which means that Angular only executes the pipe when it detects a pure change to the input value. This occurs when the input value is either a primitive value (String, Number, Boolean, or Symbol) and changes or the reference changes for an object (like an Array, Object, Date, or Function). This is similar to what we talked about above, with the inputs on the component with OnPush change detection. Impure pipes, however, execute on every change detection cycle, regardless of if the value provided to the pipe has changed or not.

While you can create and use impure pipes, it is highly recommended to not use them. They can dramatically affect the performance of your application.

Other ways that you can negatively impact the performance of your app include having complex lifecycle hooks that run on each change detection cycle, such is in ngOnChanges. If you need to use one of those lifecycle hooks, make sure its scope is limited. Your lifecycle hook might look something like this:

ngOnChanges(changes: SimpleChanges) {
if (changes.myAttribute.currentValue === ‘Some condition’) {
// Now let’s do something
}
}

As opposed to this:

ngOnChanges() {
// Do something on every change detection cycle
}

One recommendation I received was to use a TypeScript setter if you need to do something each time an Input value changes as opposed to using ngOnChanges. This ensures that the code only runs when the specific input changes and not on each change detection cycle.

Conclusion

Change detection is a core part of the Angular framework, and working to understand what it is, when it runs, and how to make properly work with it is essential in building solid, performant apps. Hopefully this article helped you along that path. Below are some resources that I used while preparing this article. Check them out if you’d like to continue learning more about change detection.

References

https://blog.angular-university.io/how-does-angular-2-change-detection-really-work/

https://medium.com/@bencabanes/angular-change-detection-strategy-an-introduction-819aaa7204e7

Lara Newsom’s Talk at ngConf 2022 (no link yet, but it will hopefully be out soon!)

Angular Change Detection was originally published in ngconf on Medium, where people are continuing the conversation by highlighting and responding to this story.

Leave a Comment

Your email address will not be published. Required fields are marked *