Skip to main content

The difference between NgDoCheck and AsyncPipe in OnPush components

An in-depth guide into manual control of change detection in Angular
Read an updated version of this article here.
This post comes as a response to this tweet by Shai. He asks whether it makes sense to use NgDoCheck lifecycle hook to manually compare values instead of using the recommend approach with the async pipe. That’s a very good question that requires a lot of understanding of how things work under the hood: change detection, pipes and lifecycle hooks. That’s where I come in 😎.
In this article I’m going to show you how to manually work with change detection. These techniques give you a finer control over the comparisons performed automatically by Angular for input bindings and async values checks. Once we have this knowledge, I’ll share with you my thoughts on the performance impact of these solutions.
I work as a developer advocate at ag-Grid. If you’re curious to learn about data grids or looking for the ultimate Angular data grid solution, give it a try with the guide “Get started with Angular grid in 5 minutes”. I’m happy to answer any questions you may have. And follow me to stay tuned!
Let’s get started!

OnPush components

In Angular, we have a very common optimization technique that requires adding the ChangeDetectionStrategy.OnPush to a component’s decorator. Suppose we have a simple hierarchy of two components like this:
With this setup, Angular runs change detection always for both A and B components every single time. If we now add the OnPush strategy for the B component:
Angular will run change detection for the B component only if its input bindings have changed. Since at this point it doesn’t have any bindings, the component will ever only be checked once during the bootstrap.

Triggering change detection manually

Is there a way to force change detection on the component B? Yes, we can inject changeDetectorRef and use its method markForCheck to indicate for Angular that this component needs to be checked. And since the NgDoCheck hook will still be triggered for B component, that’s where we should call the method:
Now, the component B will always be checked when Angular checks the parent A component. Let’s now see where we can use it.

Input bindings

I told you that Angular only runs change detection for OnPush components when bindings change. So let’s see the example with input bindings. Suppose we have an object that is passed down from the parent component through the inputs:
In the parent component A we define the object and also implement the changeName method that updates the name of the object when a button is clicked:
If you now run this example, after the first change detection you’re going to see the user’s name printed:
But when we click on the button and change the name in the callback:
the name is not updated on the screen. And we know why, that’s because Angular performs shallow comparison for the input parameters and the reference to the user object hasn’t changed. So how can we fix this?
Well, we can manually check the name and trigger change detection when we detect the difference:
If you now run this code, you’re going to see the name updated on the screen.

Asynchronous updates

Now, let’s make our example a bit more complex. We’re going to introduce an RxJs based service that emits updates asynchronously. This is similar to what you have in NgRx based architectures. I’m going to use a BehaviorSubject as a source of values because I need to start the stream with an initial value:
So we receive this stream of user objects in the child component. We need to subscribe to the stream and check if the values are updated. And the common approach to doing that is to use Async pipe.

Async pipe

So here’s the implementation of the child B component:
Here’s the demo. But is there another way that doesn't use the pipe?

Manual check and change detection

Yes, we can check the value manually and trigger change detection if needed. Just as with the examples in the beginning, we can use NgDoCheck lifecycle hook for that:
Ideally, though, we would want to move our comparison and update logic from NgDoCheck and put it into the subscription callback, because that’s when the new value will be available:
What’s interesting is that it’s exactly what the Async pipe is doing under the hood:

So which solution is faster?

So now that we know how we can use manual change detection instead of the async pipe, let’s answer the question we started with. Who’s faster?
Well, it depends on how you compare them, but with everything else being equal, manual approach is going to be faster. I don’t think though that the difference will be tangible. Here are just a few examples why manual approach can be faster.
In terms of memory you don’t need to create an instance of a Pipe class. In terms of compilation time the compiler doesn’t have to spend time parsing pipe specific syntax and generating pipe specific output. In terms of runtime, you save yourself a couple of function calls for each change detection run on the component with async pipe. Here’s for example the code for the updateRenderer function generated for the code with pipe:
As you can see, the code for the async pipe calls the transform method on the pipe instance to get the new value. The pipe is going to return the latest value it received from the subscription.
Compare it to the plain code generated for the manual approach:
These are the functions executed by Angular when checking B component.

A few more interesting things

Unlike input bindings that perform shallow comparison, the async pipe implementation doesn’t perform comparison at all (kudos to Olena Horal for noticing that). It treats every new emission as an update even if it matches the previously emitted value. Here’s the implementation of the parent component A that emits the same object. Despite this fact, Angular still runs change detection for the component B:
It means that the component with the async pipe will be marked for check every time a new value is emitted. And Angular will check the component next time it runs change detection even if the value hasn’t changed.
Where is this relevant? Well, in our case we’re only interested in the property name from the user object because we use it in the template. We don’t really care about the whole object and the fact that the reference to the object may change. If the name is the same we don’t need to re-render the component. But you can’t avoid that with the async pipe.
NgDoCheck is not without the problems on its own :) As the hook is only triggered if the parent component is checked, it won’t be triggered if one of its parent components uses OnPush strategy and is not checked during change detection. So you can’t rely on it to trigger change detection when you receive a new value through a service. In this case, the solution I showed with putting markForCheck in the subscription callback is the way to go.

Conclusion

Basically, manual comparison gives you more control over the check. You can define when the component needs to be checked. And this is the same as with many other tools — manual control gives you more flexibility, but you have to know what you’re doing. And to acquire this knowledge, I encourage you to invest time and effort in learning and reading sources.
If you’re concerned with how often NgDoCheck lifecycle hook is called or that it’s going to be called more often than the pipe’s transform — don’t. First, I showed the solution above where you don’t use the hook in the manual approach with asynchronous stream. Second, the hook will only be called when the parent component is checked. If the parent component is not checked, the hook is not called. And with regards to the pipe, because of the shallow check and changing references in the stream, you’re going to have the same number of calls or even more with the transform method of the pipe.

Comments

Popular posts from this blog

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functions, scales, axis and shape ge

JavaScript new features in ES2019(ES10)

The 2019 edition of the ECMAScript specification has many new features. Among them, I will summarize the ones that seem most useful to me. First, you can run these examples in  node.js ≥12 . To Install Node.js 12 on Ubuntu-Debian-Mint you can do the following: $sudo apt update $sudo apt -y upgrade $sudo apt update $sudo apt -y install curl dirmngr apt-transport-https lsb-release ca-certificates $curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - $sudo apt -y install nodejs Or, in  Chrome Version ≥72,  you can try those features in the developer console(Alt +j). Array.prototype.flat && Array.prototype. flatMap The  flat()  method creates a new array with all sub-array elements concatenated into it recursively up to the specified depth. let array1 = ['a','b', [1, 2, 3]]; let array2 = array1.flat(); //['a', 'b', 1, 2, 3] We should also note that the method excludes gaps or empty elements in the array: let array1

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.component