The difference between NgDoCheck and AsyncPipe in OnPush components

Post Editor

This article uses NgDoCheck and AsyncPipe to dive deep into manual control of change detection in Angular and explains what effect each method can have on performance.

7 min read
0 comments
post

The difference between NgDoCheck and AsyncPipe in OnPush components

This article uses NgDoCheck and AsyncPipe to dive deep into manual control of change detection in Angular and explains what effect each method can have on performance.

post
post
7 min read
0 comments
0 comments

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.

Let’s get started!

OnPush components
Link to this section

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:

<>Copy
@Component({ selector: 'a-comp', template: ` <span>I am A component</span> <b-comp></b-comp> ` }) export class AComponent {} @Component({ selector: 'b-comp', template: `<span>I am B component</span>` }) export class BComponent {}

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:

<>Copy
@Component({ selector: 'b-comp', template: `<span>I am B component</span>`, changeDetection: ChangeDetectionStrategy.OnPush }) export class BComponent {}

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
Link to this section

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:

<>Copy
@Component({ selector: 'b-comp', template: `<span>I am B component</span>`, changeDetection: ChangeDetectionStrategy.OnPush }) export class BComponent { constructor(private cd: ChangeDetectorRef) {} ngDoCheck() { this.cd.markForCheck(); } }

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
Link to this section

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:

<>Copy
@Component({ selector: 'b-comp', template: ` <span>I am B component</span> <span>User name: {{user.name}}</span> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class BComponent { @Input() user; }

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:

<>Copy
@Component({ selector: 'a-comp', template: ` <span>I am A component</span> <button (click)="changeName()">Trigger change detection</button> <b-comp [user]="user"></b-comp> ` }) export class AComponent { user = {name: 'A'}; changeName() { this.user.name = 'B'; } }

If you now run this example, after the first change detection you’re going to see the user’s name printed:

<>Copy
User name: A

But when we click on the button and change the name in the callback:

<>Copy
changeName() { this.user.name = 'B'; }

the name is not updated on the screen. And we know why, that’s becauseAngular 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:

<>Copy
@Component({ selector: 'b-comp', template: ` <span>I am B component</span> <span>User name: {{user.name}}</span> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class BComponent { @Input() user; previousName = ''; constructor(private cd: ChangeDetectorRef) {} ngDoCheck() { if (this.previousName !== this.user.name) { this.previousName = this.user.name; this.cd.markForCheck(); } } }

If you now run this code, you’re going to see the name updated on the screen.

Asynchronous updates
Link to this section

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:

<>Copy
@Component({ selector: 'a-comp', template: ` <span>I am A component</span> <button (click)="changeName()">Trigger change detection</button> <b-comp [user]="user"></b-comp> ` }) export class AComponent { stream = new BehaviorSubject({name: 'A'}); user = this.stream.asObservable(); changeName() { this.stream.next({name: 'B'}); } }

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
Link to this section

So here’s the implementation of the child B component:

<>Copy
@Component({ selector: 'b-comp', template: ` <span>I am B component</span> <span>User name: {{(user | async).name}}</span> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class BComponent { @Input() user; }

Here’s the demo. But is there another way that doesn't use the pipe?

Manual check and change detection
Link to this section

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:

<>Copy
@Component({ selector: 'b-comp', template: ` <span>I am B component</span> <span>User name: {{user.name}}</span> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class BComponent { @Input('user') user$; user; previousName = ''; constructor(private cd: ChangeDetectorRef) {} ngOnInit() { this.user$.subscribe((user) => { this.user = user; }) } ngDoCheck() { if (this.previousName !== this.user.name) { this.previousName = this.user.name; this.cd.markForCheck(); } } }

You can play with it here.

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:

<>Copy
export class BComponent { @Input('user') user$; user = {name: null}; constructor(private cd: ChangeDetectorRef) {} ngOnInit() { this.user$.subscribe((user) => { if (this.user.name !== user.name) { this.cd.markForCheck(); this.user = user; } }) } }

Play with it here.

What’s interesting is that it’s exactly what the Async pipe is doing under the hood:

<>Copy
@Pipe({name: 'async', pure: false}) export class AsyncPipe implements OnDestroy, PipeTransform { constructor(private _ref: ChangeDetectorRef) {} transform(obj: ...): any { ... this._subscribe(obj); ... if (this._latestValue === this._latestReturnedValue) { return this._latestReturnedValue; } this._latestReturnedValue = this._latestValue; return WrappedValue.wrap(this._latestValue); } private _subscribe(obj): void { ... this._strategy.createSubscription( obj, (value: Object) => this._updateLatestValue(obj, value)); } private _updateLatestValue(async: any, value: Object): void { if (async === this._obj) { this._latestValue = value; this._ref.markForCheck(); } } }

So which solution is faster?
Link to this section

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:

<>Copy
function (_ck, _v) { var _co = _v.component; var currVal_0 = jit_unwrapValue_7(_v, 3, 0, asyncpipe.transform(_co.user)).name; _ck(_v, 3, 0, currVal_0); }

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:

<>Copy
function(_ck,_v) { var _co = _v.component; var currVal_0 = _co.user.name; _ck(_v,3,0,currVal_0); }

These are the functions executed by Angular when checking B component.

A few more interesting things
Link to this section

Unlike input bindings that perform shallow comparison, the async pipe implementation doesn’t perform comparison at all. 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:

<>Copy
export class AComponent { o = {name: 'A'}; user = new BehaviorSubject(this.o); changeName() { this.user.next(this.o); } }

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
Link to this section

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.

Want to learn more about change detection in Angular?
Link to this section

Start with These 5 articles will make you an Angular Change Detection expert. This series is a must-read if you want to have a solid grasp of the change detection mechanism in Angular. Each article builds upon the information explained in the preceding one and goes from high-level overview down to implementation details with references to the sources.

Comments (0)

Be the first to leave a comment

Share

About the author

author_image

Principal Engineer at kawa.ai.. Founder indepth.dev. Big fan of software engineering, Web Platform & JavaScript. Man of Science & Philosophy.

author_image

About the author

Max Koretskyi

Principal Engineer at kawa.ai.. Founder indepth.dev. Big fan of software engineering, Web Platform & JavaScript. Man of Science & Philosophy.

About the author

author_image

Principal Engineer at kawa.ai.. Founder indepth.dev. Big fan of software engineering, Web Platform & JavaScript. Man of Science & Philosophy.

Featured articles