José Matos
•02 Apr 2023
Angular is a powerful and popular frontend framework used to build web applications. However, as your application grows in size and complexity, performance becomes a major concern. In such scenarios, understanding the internals of Angular’s change detection mechanism is crucial for optimizing your application’s performance.
Change detection is a mechanism that Angular uses to detect and propagate any changes that occur in a component's state to its view. When a change occurs in the component's data, Angular marks the component and its descendant views as dirty. Angular then runs a change detection cycle to propagate the changes to the view.
Change detection is essential for keeping the view in sync with the underlying data. However, if not optimized well and used correctly, it can significantly affect your application's performance.
Angular's change detection mechanism is based on a tree-like structure where each component is a node in the tree, and the DOM hierarchy corresponds to the tree's leaf nodes. In essence, every component has a corresponding view and can have child components which also have their corresponding sub-views.
When an event occurs, such as a user input, a network request or any other external input, Angular starts the change detection process.
First, Angular runs a top-down process that checks the component tree for changes. If any data has changed, the specific component, including its view and child components, is marked as dirty. After marking the components, Angular runs a bottom-up process that propagates the changes to the respective views of the dirty components.
Once the process is complete, the view is updated to reflect the changed data.
import { Component } from '@angular/core';
@Component({
selector: 'my-component',
template: `<button (click)="updateData()">Click me!</button>{{ data }}`
})
export class MyComponent {
data = 0;
updateData() {
this.data = Math.random();
}
}
In the above code example, the `MyComponent` class has a data property that is bound to the view. The component also has a method, `updateData`, that updates the data property with a random number every time the user clicks the button.
When the button is clicked, the component's data property is updated, and Angular marks the component as dirty. The dirty check triggers the change detection mechanism to run a change detection cycle, propagate the changes to the view, and update the DOM.
The default change detection process in Angular works well for small-to-medium-sized applications. However, for larger applications, the process can become a performance bottleneck.
By default, Angular performs change detection for all the components in your application during every change detection cycle, even if the component's data hasn't changed. This means that even if a component's data hasn't changed, the entire component tree still has to be traversed, checked and updated every change detection cycle.
Imagine a case where your application has thousands of components, and you are updating only one component's data. In this scenario, Angular would still perform the entire change detection on all components, which can affect your application's overall performance.
To improve performance, Angular provides us with several strategies that we can use to optimize change detection. These strategies essentially tell Angular which components to check and which ones to ignore during the change detection process.
Here are a few common change detection strategies:
The `OnPush` change detection strategy is a manual optimization technique that we can use to optimize our components. It tells Angular to check a component's data inputs only if the reference to the data input changes or if an event has been triggered on the component.
This approach ensures that components will be checked for changes only when necessary, reducing the total number of checks and improving application performance.
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'my-component',
template: `<p>{{ data }}</p>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent {
@Input() data: number;
}
In the above example, `MyComponent` uses the `OnPush` change detection strategy, which tells Angular to check the component's data inputs only if their reference changes or if an event has been triggered on the component.
Note that you need to manually trigger an event in the component to run the change detection process if the `OnPush` strategy is used.
The immutable data technique involves creating new objects or arrays every time you update your data. This way, your component's data inputs will always have new references every time they are updated.
Angular compares the references of objects and arrays to detect changes. By using immutable objects or arrays, you can be sure that your data inputs will always have different references, triggering Angular's change detection mechanism accordingly.
import { Component } from '@angular/core';
@Component({
selector: 'my-component',
template: `<p>{{ data }}</p><button (click)="updateData()">Click Me</button>`
})
export class MyComponent {
data = [1, 2, 3];
updateData() {
this.data = [...this.data, Math.random()];
}
}
In the above example, the `updateData` method creates a new array by spreading the existing array and adding a new random number to the end. By doing so, Angular will detect the change in reference and update the component's view accordingly.
One-time bindings are bindings that are only executed once when the component is created. They can be useful when rendering static content that doesn't change in the component but can significantly affect your application's performance when used inappropriately.
One-time bindings can cause Angular to generate a lot of boilerplate code, increasing the bundle size and slowing down your application's performance.
import { Component } from '@angular/core';
@Component({
selector: 'my-component',
template: `<p>{{ getData() }}</p><button (click)="updateData()">Click Me</button>`
})
export class MyComponent {
data = 0;
updateData() {
this.data = Math.random();
}
getData() {
return this.data.toFixed(2);
}
}
In the above example, the `getData` method is a costly operation that is executed every time the change detection cycle occurs. Instead of using the `getData` method directly in the view, we should use a property instead:
import { Component } from '@angular/core';
@Component({
selector: 'my-component',
template: `<p>{{ formattedData }}</p><button (click)="updateData()">Click Me</button>`
})
export class MyComponent {
data = 0;
formattedData: string;
updateData() {
this.data = Math.random();
this.formattedData = this.data.toFixed(2);
}
}
In the above modified example, the `formattedData` property is updated only when the `updateData` method is called, reducing the total number of checks during the change detection process.
Angular's change detection mechanism is a crucial part of building performant web applications. Understanding the way it works and optimizing it for your application can significantly improve your application's performance.
The techniques discussed in this article, such as the `OnPush` change detection strategy, immutable data and avoiding one-time bindings, can help you optimize your application and reduce the number of checks that Angular performs, leading to a faster and more responsive application.