Angular is a powerful and robust component-based Javascript framework for building scalable, enterprise-level web applications. But if the application grows to the point where the component tree gets big and complex, Angular’s default change detection mechanism can become a performance bottleneck. In this article, we will cover some techniques and best practices on how to improve performance.
Table of contents:
- How Angular change detection works
- Default change detection strategy
- OnPush change detection strategy
- Async pipe
How Angular change detection works
Before going in-depth into the concepts of change detection strategies, it is crucial to understand how change detection works. The Angular documentation states:
“Change detection is the process through which Angular checks to see whether your application state has changed and if any DOM needs to be updated.”
Change detection is a very optimized process in Angular but can cause slowdowns if it’s run too often and on the large component tree. During the change detection process, Angular searches for all bindings re-evaluates all expressions and compares them to the previous values. After that, it propagates the change to the DOM (Document Object Model) elements if a change is identified.
To better understand how change detection strategy works, we also have to know the difference between value types (primitives) and reference types in Javascript.
Javascript provides seven types of primitives that include number, string, boolean, undefined, symbol, bigInt, and null. All primitives are immutable, which means that they cannot be altered. When we assign a variable that stores a primitive to another variable, the value stored in the variable is created and copied into the new variable. The primitive values have a fixed size and they are stored in the stack memory.
In Javascript, the most used types of reference values are arrays, objects, and functions. The size of a reference value is dynamic. Therefore it is stored in heap memory.
This distinction is vital because to read the value of primitives, we have to query the stack memory, but to read the value of the reference type, we need to query stack memory to get the reference and then use that reference to query heap memory to get the value of the reference type.
Default Angular change detection strategy
By default, Angular performs change detection on the entire component tree (from top to bottom). This happens whenever something triggers a change in the application – either a user-triggered event(e.g. click on a button), data received from an HTTP call, or a timer being set off. To facilitate detection and update the DOM with changed data, Angular provides a change detector class for each component. Consequently creating a hierarchy of change detectors similar to a hierarchy of a tree of components. Whenever change detection is triggered, Angular goes down the tree of change detectors to check if any of them are marked as changed. Then each change detector checks the template bindings and reflects the updated data to the view. This way ensuring that both the data model and the DOM are in sync. This cycle is repeated every time a change is detected.
To make this functionality possible, we must understand that the Javascript runtime is overridable. At startup, Angular will patch several low-lever browser APIs, including addEventListener, a function that registers all browser events. Angular replaces this function with its implementation, which adds more functionality to any event handler. That gives Angular the ability to run change detection and update the template.
Patching these browser APIs is done with a library bundled with Angular called Zone.js. A zone is an execution context that persists across async tasks. Angular runs inside its own zone called NgZone. This allows Angular to detect when asynchronous tasks, tasks that can alter application state, start and finish. These tasks are the only thing that can cause views to change, and by detecting when they occur, Angular can know that the view needs an update.
OnPush change detection strategy
The main principle behind the OnPush change detection strategy is that we should treat reference types as immutable objects. This way, we can detect if a change has occurred much faster because such checks are cheaper than deep comparison checks. In that sense, every time the reference type is updated, the reference in the stack memory is changed. So detection doesn’t run automatically for every component. Angular instead listens for specific changes and only runs change detection on a subtree of that component. The default change detection only runs to the point where it comes across a component that implements the OnPush strategy.
For example, we have an array of 20 objects and want to check if there have been any changes. So if taking the immutability approach, the reference to it has to change for the array to be updated. Therefore we can check if the array reference is different immediately. This way we can save 20 more checks (in a heap) to check which element in the array has changed.
It’s important to mention, in the previous example, that we are treating reference types as immutable objects by convention. They can still be changed.
Async pipe
Another helpful technique is using the async pipe with the OnPush strategy when working with observables. If we would just subscribe to an observable in the OnInit life cycle hook with OnPush enabled, that wouldn’t work. A way to resolve this problem would be to inject ChangeDectorRef and manually call markForCheck() method. Async pipe does this for us. Under the hood, async pipe does several tasks: it subscribes to the observable and returns the last value emitted by the observable. When the value is emitted, it marks the component and its parents as dirty, waiting for the zone to trigger change detection and it automatically unsubscribes when the component is destroyed. This way prevents possible memory leaks and degradation of the performance of the angular application.