🚀 A Comprehensive Guide to Angular onPush Change Detection Strategy

Netanel Basal
Netanel Basal
Published in
6 min readMay 1, 2018

--

👉 Default Change Detection Strategy

By default Angular uses the ChangeDetectionStrategy.Default change detection strategy.

The default strategy doesn’t assume anything about the application, therefore every time something changes in our application, as a result of various user events, timers, XHR, promises, etc., a change detection will run on all components.

This means anything from a click event to data received from an ajax call causes the change detection to be triggered.

We can see this easily be creating a getter in our component and using it in our template. For example:

After the code above runs, each time we click the button, Angular will run a change detection cycle and we should see two logs of “Checking the view” in the console (or one log in production).

This technique is called dirty checking. In order to know whether the view should be updated, Angular needs to access the new value, compare it with the old one, and make the decision on whether the view should be updated.

Now, imagine a big application with thousands of expressions; If we let Angular check every single one of them when a change detection cycle runs, we might encounter a performance problem.

Although Angular is very fast, as your app grows, Angular will have to work harder to keep track of all the changes.

What if we could help Angular and give it a better indication of when to check our components?

🔥 OnPush Change Detection Strategy

We can set the ChangeDetectionStrategy of our component to ChangeDetectionStrategy.OnPush .

This tells Angular that the component only depends on its @inputs() ( aka pure ) and needs to be checked only in the following cases:

1️⃣ The Input reference changes.

By setting the onPush change detection strategy we are signing a contract with Angular that obliges us to work with immutable objects (or observables as we’ll see later).

The advantage of working with immutability in the context of change detection is that Angular could perform a simple reference check in order to know if the view should be checked. Such checks are way cheaper than a deep comparison check.

Let’s try to mutate an object and see the result.

When we click on the button we will not see any log. That’s because Angular is comparing the old value with the new value by reference, something like:

Just a reminder that numbers, booleans, strings, null and undefined are primitive types. All primitive types are passed by value. Objects, arrays, and functions are also passed by value, but the value is a copy of a reference.

So in order to trigger a change detection in our component, we need to change the object reference.

With this change we will see that the view has been checked and the new value is displayed as expected.

2️⃣ An event originated from the component or one of its children.

A component could have an internal state that’s updated when an event is triggered from the component or one of his children.

For example:

When we click on the button, Angular runs a change detection cycle and the view is updated as expected.

You might be thinking to yourself that this should work with every asynchronous API that triggers change detection, as we learned at the beginning, but it won’t.

It turns out that the rule applies only to DOM events, so the following APIs will not work.

Note that you are still updating the property so in the next change detection cycle, for example, when we click on the button, the value will be six ( 5 + 1 ).

3️⃣ We run change detection explicitly.

Angular provides us with three methods for triggering change detection ourselves when needed.

The first is detectChanges() which tells Angular to run change detection on the component and his children.

The second is ApplicationRef.tick() which tells Angular to run change detection for the whole application.

application_ref.ts

The third is markForCheck() which does NOT trigger change detection. Instead, it marks all onPush ancestors as to be checked once, either as part of the current or next change detection cycle.

refs.ts

Another important thing to note here is that running change detection manually is not considered a “hack”, this is by design and it’s completely valid behavior (in reasonable cases, of course).

🤓 Angular Async Pipe

The async pipe subscribes to an observable or promise and returns the latest value it has emitted.

Let’s see a trivial example of an onPush component with an input() observable.

When we click on the button we are not going to see the view updated. This is because none of the conditions mentioned above occurred, so Angular will not check the component at the current change detection cycle.

Now, let’s change it to use the async pipe.

Now we can see that the view is updated when we click on the button. The reason for that is that when a new value is emitted, the async pipe marks the component to be checked for changes. We can see it in the source code:

Angular is calling to markForCheck() for us and that’s why the view is updated even though the reference hasn’t changed.

If a component depends only on its input properties, and they are observable, then this component can change if and only if one of its input properties emits an event.

Quick tip: It’s an anti-pattern to expose your subject to the outside world, always expose the observable, by using the asObservable() method.

👀 onPush and View Queries

Let’s say we have the following components:

Probably your expectation is that after three seconds Angular will update the tab component view with the new content.

After all, we saw that if we update the input reference in onPush components this should trigger change detection, no?

Unfortunately, in this case, it doesn’t work that way. There is no way for Angular to know that we are updating a property in the tab component. Defining inputs() in the template is the only way to let Angular knows that this property should be checked on a change detection cycle.

For example:

Because we define explicitly the input() in the template, Angular creates a function called an updateRenderer(), that keeps track of the content value during each change detection cycle.

AppComponent.ngfactory.ts

The simple solution in these cases is to use setters and call markForCheck().

💪 === onPush++

After we understood (hopefully) the power of onPush, we can leverage it in order to create a more performant application. The more onPush components we have the less checks Angular needs to perform. Let’s see a real world example:

Let’s say that we have a todos component that takes a todos as input().

The disadvantage of the above approach is that when we click on the add button Angular needs to check each todo, even if nothing has changed, so in the first click we’ll see three logs in the console.

In the above example there is only one expression to check, but imagine a real world component with multiple bindings (ngIf, ngClass, expressions, etc.). This could get expensive.

We’re running change detection for no reason

The more performant way is to create a todo component and define its change detection strategy to be onPush. For example:

Now when we click the add button we’ll see a single log in the console because none of the inputs of the other todo components changed, therefore their view wasn’t checked.

Also, by creating a dedicated component we make our code more readable and reusable.

🚀 In Case You Missed It

Here are a few of my open source projects:

  • Akita: State Management Tailored-Made for JS Applications
  • Spectator: A Powerful Tool to Simplify Your Angular Tests
  • Transloco: The Internationalization library Angular
  • Forms Manger: The Foundation for Proper Form Management in Angular
  • Cashew: A flexible and straightforward library that caches HTTP requests

Follow me on Medium or Twitter to read more about Angular, Akita and JS!

--

--

A FrontEnd Tech Lead, blogger, and open source maintainer. The founder of ngneat, husband and father.