Angular OnPush – A Change Detection Strategy Revealing Mistakes in Your Code

When optimizing the performance of Angular applications, many developers directly associate Angular's OnPush change detection strategy with it. But, if you don't know exactly how OnPush works under the hood, it will quickly teach you what you are doing wrong the hard way. In this article, we'll look deeper into how the OnPush strategy affects Angular's change detection mechanism and which pitfalls we should avoid at all costs.

In diesem Artikel:

Angular OnPush – A Change Detection Strategy Revealing Mistakes in Your Code
Sascha Lehmann ist Developer bei Thinktecture. Sein Fokus liegt auf webbasierten Frontends mit Angular und Web Components.
All examples and code references refer to Angular 14.2.2. Also, note that all explanations only apply to Angular versions based on Ivy.
At Thinktecture, we support our customers with various problems in our daily work. Of course, the topic of performance optimization is always on the agenda. Many developers from the Angular environment associate „performance optimization“ directly with OnPush. Accordingly, OnPush is often used in many projects right from the start. But OnPush doesn’t automatically guarantee to skyrocket your application’s performance. If you lack knowledge of what it does under the hood, it can quickly turn into the opposite. It can lead to unexpected problems, which may only become apparent much later in the project and are not easy to detect. To help you avoid these pitfalls right from the start, we’ll first dive into Angular’s code to look at what OnPush does with the change detection mechanism and then head into some examples.
Before we dig into the internals of OnPush, you should make sure that you have a general understanding of what change detection is, why we need it and how Angular handles it. If you need a quick refresh or a basic introduction to the topic, I highly suggest looking at the article What’s the hype with OnPush, by my colleague Max Marschall first.

Component = View

As we all know, our components in Angular form a hierarchical tree structure. However, the change detection is not executed on the component but on a low-level abstraction layer called View or, more precisely,  LView (the term „view“ is used interchangeably with the word „component“ in the following). A view is directly associated with an instance of a component and contains additional state information, the LViewFlags. These flags are significant to decide whether a change detection cycle for the view and all its children will be skipped or not. The most important property here is CheckAlways
Note: As you might have noticed, the flags represent specific bits of a number. This mechanism performs well when storing multiple flags in a single value.

When no CheckAlways flag is present, Angular will skip the change detection checks of the view and all its children. And this is where ChangeDetectionStrategy comes into play.

Angular's two change detection strategies

The default of CheckAlways corresponds to Angular’s ChangeDetectionStrategy.Default. Here, Angular starts at the top view and recursively applies the check and update process for all child views, the tree downwards.
So ChangedetectionStrategy.OnPush must therefore unset the CheckAlways flag, right? It is not quite that simple. According to the comments in the Angular code, the following happens:
Use the `CheckOnce` strategy, meaning that automatic change detection is deactivated until reactivated by setting the strategy to `Default` (`CheckAlways`).
Change detection can still be explicitly invoked. This strategy applies to all child directives and cannot be overridden. (Angular code here)
We can confirm that quote by looking inside Angular’s ComponentFactory class (Angular code here). With OnPush, the view is marked as Dirty for checking it once; otherwise, CheckAlways is the default.
				
					const rootFlags = this.componentDef.onPush 
                  ? LViewFlags.Dirty | LViewFlags.IsRoot 
                  : LViewFlags.CheckAlways | LViewFlags.IsRoot;

				
			
However, if the @Input of the component changes, Angular will schedule checks again. The setInput function of the ComponentRef handles this. A function markDirtyIfOnPush gets called and sets the Dirty Flag on the view, so it gets checked.
				
					export function markDirtyIfOnPush(lView: LView, viewIndex: number): void {
  ngDevMode && assertLView(lView);
  const childComponentLView = getComponentLViewByIndex(viewIndex, lView);
  if (!(childComponentLView[FLAGS] & LViewFlags.CheckAlways)) {
    childComponentLView[FLAGS] |= LViewFlags.Dirty;
  }
}

				
			
At this point, we should look at the three scenarios when an OnPush component triggers a change detection:
 
  • When an @Input changes
  • When an @Output triggers 
  • When the markForCheck function gets called
So what can we take away from this? We saw that OnPush is not a performance tool per se; it is just a different strategy for Angular’s change detection that can help to reduce unnecessary change detection cycles, which may result in better performance. Especially within larger applications with many components, but it isn’t a guaranteed performance booster for every application.
 
After all this dry theory, it is time for some practical examples.

Always do it the "Angular way"

During my daily work, I stumbled over a git repository, which implements a graphical data picker component. It is an excellent example of how OnPush can show you your mistakes. I created an Angular application with the mentioned data picker in the following example (I took the code from the repository and adapted it a bit to fit the current Angular version; please feel free to play around with it).
Using ChangeDetectionStrategy.Default (which is the default), everything works smoothly. But the component seems broken when we set it to OnPush (like in the example below). Typically, the data wheel should rotate and lock when released. But what is the reason for this?
When we look at the components code, we are even more confused. There are several event handlers reacting to mouse events. When we interact with the wheel and look at the console, we see that the events are registered and doing their work.
As a result of what we have learned, a change detection cycle should trigger. But why was it not?
The error is basically where the EventHandler registration takes place. When we look at the code, we can see that the handlers directly get registered on the element with addEventListener. As a result, Angular doesn’t know that the events raise in this particular component. So, it cannot set the Dirty flag on the view and will skip the component (and its children) from change detection
				
					  addEventsForElement(el): void {
    const _ = this.touchOrMouse.isTouchable;
    const eventHandlerList = [
      { name: _ ? 'touchstart' : 'mousedown', handler: this.handleStart },
      { name: _ ? 'touchmove' : 'mousemove', handler: this.handleMove },
      { name: _ ? 'touchend' : 'mouseup', handler: this.handleEnd },
      {
        name: _ ? 'touchcancel' : 'mouseleave',
        handler: this.handleCancel,
      },
    ];

    eventHandlerList.forEach((item, index) => {
      el.removeEventListener(item.name, item.handler, false);
      el.addEventListener(item.name, item.handler.bind(this), false);
    });
  }
				
			
But how can we solve the problem? In this case, it is pretty simple. We need to register the events in the „Angular way“ instead. In this example, we should do this via template binding. Thus, the framework can map the event to the component and mark it to be checked, although using OnPush.
This small example shows us how important it is to solve our problems with the tools that Angular provides and how OnPush has shown us our mistakes that we possibly wouldn’t have discovered with the default strategy.

Mutable Objects and OnPush don't like each other

Let’s have a look at another example. A presentational component with OnPush change detection receives a person object as an input parameter. After the startup, the object displays correctly; so far, so good. Then we change a value in the form. But what do we see? Nothing happens. The child component doesn’t display the change correctly. But when we check the output in the console, everything should work correctly.
The simple reason is that OnPush-based components compare their input parameters via object comparison (Object.is(), Angular code here). In the previous case, the object itself did not change; instead, the code mutated only an object member.
				
					mutatePerson() {
    this.person.name = this.form.value.name;
    this.person.lastName = this.form.value.lastName;
    this.person.age = this.form.value.age;
    console.log('Person mutated values: ', this.person);
}
				
			
The @Input registers no change and doesn’t set the Dirty flag for the next cycle. Exactly such problem constellations can often lead to unintentionally sprawling debugging sessions. To avoid these from the outset, you should use an immutable state or immutable objects. Doing changes on immutable objects means that you don’t modify any properties but always create a new object with the changed values. It is also mandatory to treat your objects this way when using any store pattern or store-related library like ngrx.
If you are also interested in this topic, check out the articles and webinars of my colleague Yannick Baron.
Last but not least, how do we fix our example? As mentioned before, we need to replace the person object with a new one. And everything works as expected.
				
					mutatePerson() {
    this.person = this.form.value;
    console.log('Person mutaded values: ', this.person);
}
				
			

Conclusion

Finally, let’s briefly summarize our learnings. We saw that OnPush is not a performance tool per se. It just changes the strategy of Angular handling the change detection cycles. When using OnPush, we need to take the responsibility of knowing when the view actually gets updated and possibly need to update it manually. As a result, it rewards us with reduced change detection cycles and, therefore, may increase the performance of our applications. But be aware that OnPush also mercilessly shows us our mistakes, which may result in some headaches. But after you read this article now, these headaches should be history by now. To avoid them even better, follow the most important rule of performance optimization:
 
Don’t worry about optimization if you are dealing with smaller applications or not encountering issues like stuttering or frame drops.
And if you’re interested in the best possible performance from the start, keep in mind what we’ve just learned.
Kostenloser
Newsletter

Aktuelle Artikel, Screencasts, Webinare und Interviews unserer Experten für Sie

Verpassen Sie keine Inhalte zu Angular, .NET Core, Blazor, Azure und Kubernetes und melden Sie sich zu unserem kostenlosen monatlichen Dev-Newsletter an.

Diese Artikel könnten Sie interessieren
Angular
Configuring Lazy Loaded Angular Modules

Configuring Lazy Loaded Angular Modules

Making our Angular modules configurable is an important step in building a reusable architecture. Having used Angular for a while you might be familiar with the commonly used forRoot() and forChild() functions, that some modules provide you with. But what is the best way to provide configuration in these cases?
16.06.2022
Angular
Master Web Component Forms Integration – with Lit and Angular

Master Web Component Forms Integration – with Lit and Angular

When a company has cross-framework teams, it is a good choice to use Web Components to build a unified and framework-independent component library. However, some pitfalls are to consider when integrating these components into web forms. Therefore, for a better understanding, we will look at two possible approaches and try to integrate them into an Angular form as an example.

Notice: All code samples are available on Github!
09.06.2022
.NET
Asynchrone Operationen: Blazor WebAssembly für Angular-Entwickler – Teil 5 [Screencast]

Asynchrone Operationen: Blazor WebAssembly für Angular-Entwickler – Teil 5 [Screencast]

Eine Webanwendung will natürlich auch mit Daten gefüttert werden. Doch diese müssen irgendwo her kommen. Nichts liegt näher als diese von einer Web API zu laden. Dieser Screencast zeigt, wie asynchrone Operationen in Blazor funktionieren und welche gravierenden Unterschiede es zu Angular gibt.
26.05.2022
.NET
Typings: Blazor WebAssembly für Angular-Entwickler – Teil 4 [Screencast]

Typings: Blazor WebAssembly für Angular-Entwickler – Teil 4 [Screencast]

C# und TypeScript entstammen der Feder der selben Person. Doch sind sie deshalb auch gleich? In diesem Teil der Screencast-Serie erfahren Sie, wie mit Typen in den beiden Programmiersprachen verfahren wird und welche Unterschiede es gibt.
19.05.2022
.NET
Bindings: Blazor WebAssembly für Angular-Entwickler – Teil 3 [Screencast]

Bindings: Blazor WebAssembly für Angular-Entwickler – Teil 3 [Screencast]

Wer Komponenten einsetzt, steht früher oder später vor der Fragestellung, wie man Daten an die Komponente übergibt oder auf Ereignisse einer Komponente reagiert. In diesem Screencast wird gezeigt wie Bindings bei Komponenten funktionieren, also wie eine Komponente Daten von außerhalb benutzen und Rückmeldung bei Aktionen geben kann.
12.05.2022
.NET
Komponenten: Blazor WebAssembly für Angular-Entwickler – Teil 2 [Screencast]

Komponenten: Blazor WebAssembly für Angular-Entwickler – Teil 2 [Screencast]

Bei der Entwicklung einer Webapplikation kommt es ständig vor, dass UI-Teile immer und immer wieder verwendet werden. Damit nicht immer Copy & Paste verwendet werden muss, können diese Teile in Komponenten zusammengefasst werden.
05.05.2022