Understanding how Angular ChangeDetectionStrategy.OnPush works for @Input, Service and ng-content

Observe and analyze by debugging step by step through ChangeDetectionRef internal methods

Karthik Gotrala
10 min readOct 24, 2020
Photo by Ross Findon on Unsplash

Introduction:

Whenever we talk about performance in Angular based application, Change Detection Strategy plays an important role. In this article, we will observe and analyze how change detection works by going through a couple of cases where each case has component(s) laid out in a particular format and uses either Default or OnPush Strategy.

Overview:

In the Angular framework, the view is automatically updated anytime the underlying component’s data is changed. This is achieved by triggering angular change detection where angular provides a change detector for each component. By default the change detection is automatic and the detection cycle runs multiple times within a set period of time and trigger conditions are many.

One of the heaviest operations through the change detection cycle is re-rendering the view (paint operation). When you have n number of components and if the change detection runs x number of times where x is dependent on various trigger conditions such as mouseover, click, setInterval, timeout or underlying HTTP call, etc.,

As the n increases then n*x the value increases exponentially which ultimately is very bad for the overall performance of the application. Mathematically it may not be an accurate representation but it gives you an overview of what kind of impact that it will make on the overall performance of the application. The framework provides two options i.e., ChangeDetectionStrategy.Default (CheckAlways) and ChangeDetectionStrategy.OnPush( CheckOnce).

When a component doesn’t explicitly define the ChangeDetectionStrategy then Default is selected and as the name suggests the change detection cycle runs every time and re-renders the view irrespective of whether underlying data is changed or not. However, with OnPush the automatic change detection is disabled but runs only when underlying data is changed or when invoked explicitly by the component via methods exposed by ChangeDetectionRef abstract class.

Now that we had a quick overview, let’s go through a couple of use cases and observe how change detection works:

Use Case 1: OnPush vs Default when passing data from parent to child components via @Input()

Code Snippet of AppComponent (app-root) where it has two child components with one default and OnPush change detection strategy. The DefaultBoxComponent and OnPushBoxComponent where other than changeDetectionStrategy everything else is the same in typescript and HTML template.
Pictorial illustration for components with two different ChangeDetectionStrategy implementation

As shown in the above code snippets and picture, created two components:

1. Both DefaultBoxComponent & OnPushBoxComponent and the associated template has almost the same code except in the case for changeDetectionStrategy they are set as Default and OnPush, respectively. (see line 11,10)

2. AppComponent passes boxes array data to these components and the corresponding variable is declared with @Input() decorator (see line 15,14)

3. AppComponent has two buttons, Add and No Action where on click of these buttons the former will push an item into the boxes array and later does nothing, respectively.

Let’s run the app and see:

On click of the Add button, a new item is added into the boxes array. Here, DefaultBoxComponent alone is updated.

Observation:

When we click on the Add button, a new item is added to the boxes array and at the same time view is also updated accordingly for DefaultBoxComponent but not for OnPushBoxComponent although the same boxes data object is passed by the parent AppComponent?

Reason:

If you observe both these components, the only variation is the ChangeDetectionStrategy where for DefaultBoxComponent it was set at Default( a.k.a CheckAlways) while for OnPushBoxComponent it was OnPush (a.k.a CheckOnce).

If we look at angular documentation or comments for each enum as in the angular code below, it says when the strategy value is 1 (Default), the change detection is automatic but when the value is 0(OnPush) then automatic detection is deactivated but can be invoked explicitly.

Unlike in Default mode, the change detection in OnPush mode doesn’t automatically update all of its child components change unless otherwise marked for dirty or change detection triggered explicitly.

Angular Code ChangeDetectionStrategy abstract class

Invoking Change Detection Explicitly:

ChangeDectectorRef class allows us to mark a specific view dirty so that the view can be re-rendered in the next change-detection cycle.

After reading through the angular documentation, I came across how we can use DoCheck angular life cycle hook to implement custom change detection.

Step 1: Update OnPushBoxComponent to implement DoCheck life cycle hook.

Step 2: Implement ngDoCheck() method. Here, I just added console.log to see when and how this lifecycle hook was getting called.

Step 3: Repeated the above two steps for DefaultBoxComponent and AppComponent.

Implemented DoCheck angular life cycle hook. ngDoCheck() is called whenever change detection run.

Observation:
On Click of Add button the boxes array was updated with new item. DefaultBoxComponent sections view updated and we can see the new items added but the OnPushBoxComponent view remains unchanged. However, on the right side console window, we can see that ngDoCheck hook is called for the root component and all its children which is evident via the log entry.

Analysis:

The change-detection trigger originates from the root component i.e., AppComponent, and propagates to all child components. Every time component’s change detection triggers the DoCheck lifecycle hook is called and based on the strategy declared view is re-rendered or not.

On Click of Add Button, the boxes array is updated with a new item. Since AppComponent set with Default strategy, change detection triggers, and as designed its associated ngDoCheck hook is called, and then change detection propagates to the rest of the child components. Since DefaultComponent is set with Default a.k.a CheckAlways strategy, the view is re-rendered automatically with the latest information. However, since OnPushCompoent has OnPush a.k.a CheckOnce Strategy, only life cycle hook is called but the view is not re-rendered and it is the responsibility of the component to decide whether it should be marked as dirty for the component view to be re-rendered.

We can mark a view as dirty using the ChangeDetectionRef abstract class methods. It exposes the following few methods:

1. markForCheck: It can be used to mark a component dirty i.e., in need of re-rendering. The view is updated only when the next change detection runs.

2. detach: To detach from change detection even if they are marked dirty until they are attached back

3. detectChanges: Runs the change detection for the corresponding component and all its children.

4. checkNoChanges: Can be used to check if there any changes associated with the component and throw an exception if any changes were identified

5. reattach: Attach the view back to the change detector tree.

Updating the OnPushBoxComponent code to trigger custom change detection as below:

Implement custom change detection via DoCheck hook where if the condition matches then mark the component dirty via markForCheck

Note: In OnPush mode, instead of doing a custom change detection we can simply change the new instance of boxes array before adding a new item. Since the object reference has changed the @Input() set method gets called which inherently re-renders the view as the underlying data got changed. But, since I wanted to explain how to trigger change detection explicitly, used the above approach as shown code snippet.

Let's run the app and see:

OnPushComponent section is now getting updated with data passed by the AppComponent after implementing custom change detection via ChangeDetectionRef.markForCheck()

Step through the code changeDetection.markForCheck() implementation:

Stepping through the markForCheck implementation

Observation:
When ever a component is marked for check i.e., dirty, not only the current component but also all of its parent components will be marked as dirty. Here only the components are only marked dirty but the view is rendered only in next change detection cycle which may be immediate or may be at later time that may ultimately depend on the user’s action.

Analysis:

As seen in the above video, the markViewDirty, which is a function that markForCheck calls internally have the following implementation:

  1. Start a while loop and mark the current view as dirty until the lView is not null. Here the lView is the state of the current view that is being processed. It contains an array of objects that may comprise of text, element container, or any local variables defined within the component.
  2. For the current lView, get the parent lView and mark it as dirty (see line 13594 where lView[FLAGS]|=64 a Bitwise OR operation performed to set the flags to attribute to mark it as dirty). If the current lView is the root view then exit the loop. Most of the time the root view will be the parent of the app-root (AppComponent).

Step through the changeDetectionRef.detectChanges() implementation:

Using detectChanges() instead of markForCheck() from changeDetectorRef abstract class

Observation:
Using changeDetectionRef.detectChanges() actually triggers change detection which essentially re-renders the associated view irrespective of whether component is marked dirty or not.

Analysis:

detectChangesInternal() which the function that ChangeDetectorRef.detectChanges() uses actually refreshes view via refreshView and all its children with the latest changes and re-renders the view via executeTemplate irrespective of view was marked for dirty or not.

Case 2: Nested Components with the same data shared across components via a service class with changeDetectionStrategy.OnPush.

A Pictorial illustration of components that are laid out nested i.e., child to a parent with changeDetectionStrategy.OnPush

From the above picture, we can see that the AppComponent has reference to BoxOneComponent which has reference to BoxTwoComponent which in turn has reference to BoxThreeComponent.

Code snippet for one of the box component where all the rest of the nested components have a similar implementation with changeDetectionStrategy.OnPush

Let’s run the app:

Demo of the final components that got rendered. Here on click of the button on the respective component will increment the counter in the BoxService class

Observations:
When Box One button is clicked, counter in the boxService class is incremented but only the BoxOneComponent view is updated. However, when Box Two button is clicked both BoxOneComponent and BoxTwoComponent view are updated to reflect latest count. Similarly, when Box Three button is clicked all three component views are updated to reflect the current counter value.

Click of ‘Box one/Box two/ Box Three’ button resulted in calling markViewDirty() function internally which marked current component’s ancestors as dirty (i.e., requires view re-render)

Analysis:

Unlike in Default the change detection in OnPush Mode doesn’t automatically update all of its change unless otherwise marked for dirty or change detection triggered explicitly.

After stepping through the code, I observed that on click of the button, markViewDirty was getting called internally which by design marks all parent components as dirty (i.e., require view re-render).

Hence by design, when BoxThreeComponent’s Box Three button was clicked all its ancestors i.e., BoxTwoComponent and BoxOneComponent were also marked as dirty hence the view got reflected with an updated counter value from the BoxService class.

Likewise, when BoxTwoComponent’s Box Two only BoxTwoComponent and BoxOneComponent were updated but not BoxThreeComponent as it was a child to BoxTwoComponent and not a parent(ancestor).

Case 3: Projected Components via ng-content with Change detectionstrategy.OnPush and the same data shared across all components via a service class.

A pictorial illustration of components that look as if the components are laid as they are nested components.

In this case, the components are mentioned within the app.component.html as a nested element instead of referring to the components inside the respective components HTML template. Using the ng-content, the components are projected dynamically to render a view similar to case 2.

Let's see how change detection works?

Using ng-content the components are projected to look as if they are hierarchial tree.

Observation:
On click of the Projected Box Three button, counter in box service is updated and only box three projected component view got updated with latest value. Likewise, similar pattern was observed for Projected Box Two and Projected Box One respectively

Analysis:

After debugging through the markViewDirty, we can see that even though it looks like projected-box-one is parent of projected-box-two which in turn is the parent for projected-box-three but due to content projection via ng-content, all the above three components are actually the direct child of app-root component. Hence when markViewDirty is triggered only mat-button (button)->projected-box-three->app-root are marked dirty when project-box-three button was clicked due to which only projected-box-three the view alone is re-rendered to display updated value.

Code snippet detailing the case3 implementation where components are projected via ng-content but look as if they are nested in the rendered view

Summary:

  1. We walked through a couple of use cases observed how change detection behaves.
  2. We observed that in default mode whenever the change detection cycle runs, the current component, and all of its child components view is refreshed.
  3. With the OnPush strategy, when a component is markedForCheck, then along with the current component all of its ancestor components are marked for dirty and all its view gets re-rendered when the next change detection cycle runs
  4. When the components are projected via ng-content, even though they seem to look like they are nested components whatever observations that were made in case 2 doesn’t apply here as each projected component are a direct child of app-root component and hence on click of the button only associated component got re-rendered unlike in case 2.

Thank you for reading through the article, I hope this article provides a little better understanding of how change detection works in Angular. If you liked the article, please do clap/comment and share it with your friends/colleagues. If there any changes or corrections needed, please do comment, and will try to address as much as possible.

Resources:

  1. https://angular.io/api/core/ChangeDetectorRef#changedetectorref
  2. The pictorial illustrations were created using subscribed Microsoft PowerPoint

--

--

Karthik Gotrala

Full Stack Software Developer, Angular,Java, Web Accessibility,father, husband,son...