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.”
Angular change detection and runtime optimization
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.
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.
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.
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.