When starting a new Angular application, the default change detection used in every component is ChangeDetectionStrategy.Default
. This means that Angular by default checks every part of the application for changes when for example user events are fired, API requests are made or timers are active.
To show this in action, I've mocked a small ToDo list example (it's gotta be todos again right?):
import { Component } from "@angular/core";
@Component({
selector: "app-todo-list",
template: `
<h1>Some todos 📝</h1>
<button (click)="add()">Add Todo</button>
<div class="todos">
<app-todo [todo]="todo" *ngFor="let todo of todos"></app-todo>
</div>
`,
styles: [
`
.todos {
margin-top: 1.4rem;
display: flex;
flex-direction: column;
}
button {
border: solid #193549 2px;
}
`
]
})
export class TodoListComponent {
public todos = [{ title: "Number 1" }, { title: "Number 2" }];
public add() {
this.todos = [...this.todos, { title: `Number ${this.todos.length + 1}` }];
}
}
TodoListComponent.ts
import { Component, Input } from "@angular/core";
@Component({
selector: "app-todo",
template: `
<div class="todo">{{ todo.title }} -> {{ didChangeDetectionRun }}</div>
`,
styles: [
`
.todo {
margin-bottom: 0.5rem;
}
`
]
})
export class TodoComponent {
@Input() todo;
get didChangeDetectionRun() {
const date = new Date();
return `Change detection was run at
${date.getHours() < 10 ? "0" : ""}${date.getHours()}:
${date.getMinutes() < 10 ? "0" : ""}${date.getMinutes()}:
${date.getSeconds() < 10 ? "0" : ""}${date.getSeconds()}
`;
}
}
TodoComponent.ts
When a Todo is added via the Add Todo button, Angular checks the complete tree for changes, e.g. every TodoComponent checks if changes to the passed todo happpened. The time where the change detection was run is displayed next to a todo, and you can see that it's the same for every todo.
Oh no, change detection ran for all of our todos :(
Now, since only one todo got added, we would want to leave the previous todos untouched, right?
We can do this by setting the changeDetectionStrategy of the TodoComponent to OnPush.
import { Component, Input, ChangeDetectionStrategy } from "@angular/core";
@Component({
selector: "app-todo",
template: `
<div class="todo">{{ todo.title }} -> {{ didChangeDetectionRun }}</div>
`,
styles: [
`
.todo {
margin-bottom: 0.5rem;
}
`
]
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoComponent {
// Some unchanged code.
}
Updated TodoComponent.ts
Now when a todo is added to our list, the previously rendered todo components don't go through change detection, because their Input Reference hasn't changed.
In this small example, this wouldn't really make a big difference in performance. However, in a big application with a lot of expressions, the change detection might be unnecessarily slow if everything is checked all the time.
With OnPush
, Angular only checks for changes in the following cases:
- The input reference for a component changes, like in our previous example
- An event originates from the component or from one of its children. This for example is the case when you have a binding in a template like
<button (click)="onSave($event)">Save</button>
- When change detection is run explicitly, e.g. you put a
detectChanges()
in your component. - When the async pipe is used in a template and a new value passes through an observable, the change detection is done implicitly.
In the application I'm currently working on, we've now started to implement these changes as a means to improve overall performance, especially in complex views with a large number of data bindings.
I hope this helps with understanding the change detection behaviour of Angular a bit better. I've mostly just written this down to remember it myself after researching how most of it works, but maybe it helps some of you :)
Thanks for reading and have fun coding.
Top comments (0)