Angular has a Change Detector mechanism attached to each component that is responsible for synchronizing data between the component and its theme. It is thanks to this mechanism that the DOM tree of our entire application reflects the current state of the application.
We have two change detection strategies available (ChangeDetectionStrategy):
- Default
- OnPush
The third, the most radical option is to completely disable automatic change checking by deactivating the ngZone library responsible for notifying the detector about the events. This option, however, goes beyond the scope of this post.
In what situations does ngZone trigger Change Detector:
- any event originating from the browser (click, keyup, onmouseover, etc.)
- setInterval() and setTimeout()
- HTTP requests using XMLHttpRequest
- Promise.then()
- WebSocket.onmessage()
- @Input change:
- Default – value change
- OnPush – only reference change
We must be aware that if some asynchronous API is not supported by Zone.js (such as IndexedDB callbacks), we will have to take care of running the Change Detection mechanism ourselves.
In a production application, the Change Detector is run once after a Change event occurs. In development mode, on the other hand, it is run twice to detect potential problems related to the fact that the value has already changed after Angular has redrawn the DOM tree.
Once the Change Detector has been run on a particular component, it checks each template expression (i.e. the values contained in {{ expression }}
or as binding [property]="expression"
) to see if its value has changed. The way of comparing values that occurs here is strict equality ===
.
Default data refresh strategy
In the default configuration, when Change Detector is run, all components in the tree are checked in order from top to bottom. For larger applications with a large number of components or a large amount of data, this strategy may have a negative impact on the application performance.
SOnPush data refresh strategy
The OnPush configuration allows us to optimize the number of Change Detector mechanisms run (they are not run for all components in every case). Components and their children are not updated always, but only when:
- the input reference has changed (changing the value is not enough in this case)
- it is from this component or its children that the event has been initiated
- change detection was triggered manually
- observable used in the template via async pipe emitted a new value
The OnPush strategy works best with a immutability data approach.
Debugging
Thanks to the option available in the tools for developers in the Google Chrome (Rendering – Paint flashing) we can see in real time which elements of the page are being redrawn by the browser engine, and then make optimization where it will give the greatest benefit.
Change Detector methods
ChangeDetectorRef.markForCheck()
– marks all components from root to source as dirty, Change Detector will run on them the next cycle, even if they are OnPush components.
ChangeDetectorRef.detach()
and ChangeDetectorRef.reattach()
– we can disable Change Detection for a given component and only call it with detectChanges()
in cases we specify.
ChangeDetectorRef.detectChanges()
– triggers Change Detection for the component and its children.
ChangeDetectorRef.checkNoChanges()
– throws an error if changes were found by Change Detector during the current cycle
ngZone.runOutsideAngular()
– run the code outside the ngZone if you do not want the Change Detector to be called in this case.
Additional ngZone configuration
Interestingly, it is also possible to set the ngZone in more detail by creating a zone-flags.ts
configuration file. We can use it to specify events, which we do not want to trigger Change Detection. Detailed description of these settings can be found in ngZone documentation.
Application code optimization
Once we have an understanding of how Change Detection works in Angular, it’s worth looking at whether the code we write behaves optimally in the context of this mechanism.
Template functions
The Change Detection mechanism makes it necessary to call a function in the view each time to find out its result value and compare it with the current one. Very often it is an unnecessary, redundant call. Every time you add a function to a view it is worth considering whether it will be redundantly called. Ways of possible optimization:
ngOnChanges
lifecycle hook orsetter
– save the result of the calculation to a separate property and recalculate it only in specific cases,pure pipe
– each pipe is pure by default, i.e. the value it returns is recalculated only if any of its inputs changes.
trackBy function
trackBy
allows you to optionally pass to the ngForOf
directive a function that uniquely identifies the unique identifier of each list element and allows you to rebuild only the portion of the list’s DOM tree that has actually changed.