Understanding Change Detection Strategies in Angular

Greatly optimize your Angular app performance using smart change detection strategies.

Chidume Nnamdi 🔥💻🎵🎮
Bits and Pieces

--

Introduction

Change Detection and its strategies (OnPush and Default) have been written about in various articles in the web but no one have come close to provide comprehensive and in-depth information about the concepts, they mostly focus on the use cases.

This article goes beyond it to provide you not only the in-depth knowledge about change detection and change detection strategies (OnPush and Default) but also, the mechanics how each of the concepts are implemented.

We will go through real-world use cases and at each use case, we will look into the sources to see how/why it works.

What will we gain in this article? We will acquire in-depth knowledge of Angular change detection strategies and why the various use cases affect each concept. Also, with this knowledge, we will be able to hugely optimize the performance rate of our apps.

Tip: Use Bit (GitHub) for your components. It will help you organize, share and reuse them between apps to build faster. It’s also fun to discover your components in a visual collection, try the playground and install in the code.

NB: Throughout this article, we will refer to change detection as CD.

View and Tree of Views

An Angular application is a tree of Components. And Components are the views Angular renders, so it can be put in another way, Angular is a tree of views.

A View is a fundamental building block of the application UI. It is the smallest grouping of Elements which are created and destroyed together.

An Angular Component is a single View. A component is composed of three parts:

  • Markup (View)
  • Metadata (Decorator)
  • A class (Controller)

All this combined together lets us create a new HTML language.

Components are a feature of Angular that let us create a new HTML language and they are how we structure Angular applications.

Tree of Views

As we stated earlier, an Angular app is nothing but a tree of components. The bootstrapping/rendering of the app starts from the root of the tree up to the “branches”.

Components are structured in a parent/child relationship, when each component renders, its children Components are rendered recursively.

For example, we have five components: AppComponent, BookListComponent, BookComponent, FoodListComponent, FoodComponent.

AppComponent is the root component with children Components BookListComponent and FoodListComponent. BookListComponent is parent to BookComponent and FoodListComponent is parent to FoodComponent.

The tree of our app can be represented like this:

When [app] is rendered, it renders [food-list] then, [food]. Next, [book-list] is rendered and followed by [book]. At the end we see something like this:

As we already know a Component == a View. Each View has a state, that decides whether to update its UI and its children or simply ignore it.

Change Detection Cycle

Change Detection refreshes the DOM tree to display the changed states in an app.

The goal of change detection is always projecting data and its change.

Angular uses Zone.js to detect when to trigger a CD cycle. Angular actually wraps Zone.js functionality into a class NgZone. Zone.js emits an async event whenever an async operation is executed. Async operations are emitted by:

  • setTimeout, setInterval, etc
  • Events like click, drag, mouseover, etc
  • XMLHttpRequest

Angular runs a CD cycle whenever it detects any of these events through Zone.js. Angular CD cycle is triggered by a method tick in the ApplicationRef class.

It loops through the _views array and calls their detectChanges method. We see the _views are of type InternalViewRef. InternalViewRef is a subclass of ViewRef:

The ViewRef class embodies a component view in Angular. A Component declared in Angular is represented by a ViewRef class, manipulations in the Component’s view is done through its ViewRef class instance.

ViewRef extends ChangeDetectorRef class:

So we see internalViewRef has all properties and methods of ViewRef and ChangeDetectorRef classes.

When we want to manipulate CD from a Component, we inject the ChangeDetectorRef class.

The ChangeDetectorRef is an abstract class as we saw above, so on injection:

The createChangeDetector function returns an implementation of ChangeDetectorRef.

ViewRef_ class implements the interface InternalViewRef and defines the methods InternalViewRef inherited from its parent classes ChangeDetector and ViewRef:

So ViewRef_ can be of types ChangeDetector, ViewRef or InternalViewRef. The createChangeDetectorRef function returns an instance of ChangeDetectorRef class, it will contain only the methods in the abstract ChangeDetectorRef class, but now defined. Methods destroy, attachToViewContainerRef, attachToAppRef, detachFromAppRef, onDestroy, destroy, destroyed won’t be available.

The detectChanges() method is what is called in the tick() method. The detectChanges method in turn calls a fucntion, checkAndUpdateView:

The checkAndUpdateView is responsible for the change detection cycle. It recursively walks through the Angular’s tree of views, checking and updating each component’s view.

1. This checks the BeforeFirstCheck and FirstCheck state of the current view. If its the very first check it zeroes out the BeforeFirstCheck state and enables the FirstCheck state. Then, on the next CD run, it becomes the view’s first check so the previously enabled FirstCheck flag is zeroed out. The initial state of any view is CatInit.

The CatInit has the BeforeFirstCheck, CatDetectChanges(enables Attached and ChecksEnabled flags) and InitState_BeforeInit flags set.

Read more about bitmasks here.

2, shiftInitState function is called before each cycle of a view’s check to detect whether this is in the initState for which we need to call ngOnInit, ngAfterContentInit or ngAfterViewInit lifecycle methods.

Lifecycle hooks are methods provided by Angular to let users run code in every step of a Component/Directive lifecycle.

The lifecycle hooks are:

  • OnInit
  • OnDoCheck
  • OnChanges
  • OnDestroy
  • ngAfterContentInit
  • ngAfterViewInit
  • ngAfterContentChecked
  • ngAfterViewChecked

Here, we are checking whether to set InitState_CallingOnInit flag on the view state if the view currently has InitState_BeforeInit state set.

3 This marks projected views for check. Projected views are elements in the ng-template tag marked as ProjectedView that it is attached to a container. This function loops through the projected views and set the ViewState.CheckProjectedView flag on each view.

4. checks and updates the Directives changed input @Input() decoratedproperties. The first param is the current view object and the 2nd param is the type of action to be taken, ViewAction.CheckAndUpdate. ViewAction.CheckAndUpdate tells Angular we want to check the node for any changes if any update the Directive class properties with the changed values.

5. This function call runs CD for Embedded views in the view. Embedded views are generated whenever we use the ng-template tag.

6. Runs updates on @ContentChild and @ContentChild queries in the view.

7. The shiftInitState is checking whether to set InitState_CallingAfterContentInit flag if the InitState_CallingOnInit flag is set in the current view state.

8. Here, ngAfterContentInit lifecycle hook is called on the view.

9. This update the bindings on elements in the current view if any have changed.

10. This runs CD on the view’s children recursively, ie this checkAndUpdateView is called again with the child view as the argument.

11. ViewQuery nodes in the current view are checked for changes.

12. InitState_CallingAfterViewInit flag is set on the view if it already has InitState_CallingAfterContentInit flag enabled.

13. ngAfterViewInit and ngAfterViewChecked lifecycle hooks on the view are called here.

14. If the current view has OnPush CD strategy flag set on it. The ChecksEnabled flag set on its state is disabled.

15. Here, flags CheckProjectedViews and CheckProjectedView are disabled on the view.

16. InitState_AfterInit flag is set if the view has InitState_CallingAfterViewInit flag already present.

View States and Change Detection States

View States Each view in an Angular tree of views has possible states it can be in. These states decide whether to run a UI update/CD on a view and its children.

The possible states a view can be in:

We will see how this states affect CD run on a view in the later in this article.

Change Detector Status This defines the possible state of the change detector.

CheckOne: This employed by the OnPush CD strategy. It checks the component once and sets its state to Checked.

Checked: This is the state to which OnPush components are set after the initial CD run on them.

CheckAlways: This is employed by Default CD strategy. CD is always run on the components.

Detached: The state to which components are set when they are detached from the CD tree. In this state, CD run on the view and its children are skipped.

Errored: This state indicates that CD encountered an error on the view. This is caused when a directive’s lifecycle method call throws or a binding in the view has issues. In this state, a CD is skipped in this view.

Destroyed: This state indicates the component has been destroyed.

Default CD Strategy

As the name implies, it is the default CD strategy every view has. It is set whenever we create a Component via @Component decorator:

This the configuration metadata for an Angular component. We see that it has changeDetection property. This is what is set to tell Angular that our view has either OnPush or Default CD strategy. It sets the CD strategy to use when propagating the component’s bindings.

Here, we see the Component decorator changeDetection property is set to ChangeDetectionStrategy.Default even before we use it. So, whenever we docorate our class with @component the changeDetection strategy is set to Default.

But, if we want to change it, we can override it by simply doing this:

This changes the CD strategy to OnPush removing the initial Default setting.

With Default CD strategy, our Component is checked always in every CD run until it is deactivated.

Now, imagine our app grows to be complex with hundreds (or more) of bindings (template bindings, property bindings, query bindings) to update on every single CD run. This will hugely impact the performance of our app.

But, what if we could tell Angular when to run CD? Wouldn’t that be ideal? I guess it would.

OnPush CD Strategy

OnPush CD strategy tells Angular to run CD on the Component for the first time and deactivate. Deactivate? Yes, on subsequent CD runs the Component will be skipped.

OnPush CD strategy is set on a Component like this:

The component factory will look like this:

The first arg in the viewDef is the ViewState. During CD cycles this is the param Angular checks to see if would skip or run CD on a view.

OnPush Triggers

There are cases whereby views with OnPush CD strategy can be run explicitly even after the initial CD run and deactivation. We will look at the cases below:

DOM Events

DOM Events cause CD to run on a view with OnPush Strategy.

For example:

When the Click button is clicked the count increments and the update will be reflected on the DOM. DOM events are async ops and setTimeout, XHRs etc are also asynchronous. But only DOM events runs UI updates on an OnPush component, other APIs won’t work.

The setTimeout function increments the count variable, but it won’t be updated on the DOM. When we click the button the count value( plus the increments made by the setTimeout) will be reflected on the DOM.

Why, is this so? Why should it be only DOM events? To know the reason let’s peek under the hood.

The factory generated by the DOMComponent will look like this:

The outputs array holds the events to be registered against the button element. The handleEvent function is the global callback function for any event triggered by the button element. During view creation, Angular loops through the outputs array and attaches each event in the outputs array to the element.

The listen call registers the event output.eventName to the element listenTarget || el with handleEventClosure as the callback function. The handleEventClosure function is a closure which from the renderEventHandlerClosure function. As handleEventClosure == (event: any) => dispatchEvent(view, index, eventName, event). The dispatchEvent is called on any event emission (in our case a click event). The dispatchEvent function

will call markParentViewsForCheck(startView); and eventually call our handleEvent function.

The markParentViewsForCheck function

enables the ChecksEnabled flag on components with OnPush CD Strategy an iterates upwards to its parent views enabling any view with the OnPush CD Strategy.

So when the callback function has exited, CD is run from the root view via tick(). Our component being set to ChecksEnabled will have its UI updated. That’s CD will be run on our component because it will pass this check:

Then after that it will be disabled:

Remember, OnPush runs once and deactivates. We have seen why DOM events update UI on OnPush components but not by other async APIs.

detectChanges() call

The detectChanges() method is a method in the ChangeDetectorDef abstract class defined in the ViewRef_ class a subclass of ChangeDetectorRef.

This method runs CD on a view and its children.

For example:

We injected the ChangeDetector class so we could access and run its detectChanges method.

So, how would it run CD on this component whereas, it has already run once and been deactivated?

We can trigger CD from two places in Angular:

  1. from ApplicationRef class via the tick method.
  2. from ChangeDetector class via the detectChanges method.

The tick method runs CD from the root view, whereas detectChanges runs from the passed in view downwards to its children.

The checkAndUpdateView is the function that runs the CD and recursively calls its children. Looking back at the checkAndUpdateView function implementation in the Change Detection Cycle section. We will see that UI updates 9. and directives 4. are performed first before the ChecksEnabled flag is disabled on OnPush components 14.. So if we could jump the gun and somehow manage to run from 4. through to 9., we will have UI updated on an OnPush component even though deactivated. We will have to find a way to call the checkAndUpdateView function with our OnPush view as the parameter.

Looking at the detectChanges method, we will see that

it calls the checkUpdateANdView function with a view arg passed to it. So if we pass our OnPush view to it, the view’s UI will be updated and the changes made to it will be rendered, yet it has long since been deactivated.

That’s the reason we injected ChangeDetectorRef in our component, so we could explicitly run CD on our OnPush view via the checkAndUpdateView function.

Immutability check

How does this run CD on a deactivated OnPush component? we will see in a while.

Let’s say our component has an input binding to receive data from its parent:

and on the parent component, we have this:

The IComponent has an input iHuman to receive an object from its parent PComponent. Now, if we click on either Incr. Id or Change Name button, the view on IComponent won't be updated to reflect the new change on its property iHuman.

Why? There was no reference change, the object was mutated directly.

Although, IComponent shouldn’t have been updated ‘cos it is an OnPush component and has already been run once and deactivated. So why was it run? Simple, it was because of the input binding <my-i [iHuman]="pHuman"></my-i> on its parent's template.

See the tree of the app:

CD runs from the top app-root down to my-i. Next, let's look at the factory generated for the PComponent:

During CD pass in the app-parent PComponent, the updateDirectives arg is called (check 4. in checkAndUpdateView function) via Services.updateDirectives. This function updates all directives bindings in the current view. We see that the updateDirectives arg takes in _ck and _v, _ck refers to checkAndUpdateNode function and _v is the view definition of the view. It retrieves the current value of the bound variable pHuman and calls _ck with it along with other parameters, _v, 1 the index of the directive node to update, 0 the type of update to perform, currVal_0 the value to update the directive node with.

In the process of updating the directives properties,

The value to be updated is checked against the value in the directive’s class in the checkBinding fucntion.

looseIdentical uses the equality check === to check for reference change. If any it returns true, if not, false is returned. So if our object's reference was changed, the changes variable in the checkAndUpdateDirectiveInline function will be set to true, then, the directive's property will be updated via the updateProp function.

Here, the component view is retrieved and if has OnPush CD strategy, the ChecksEnabled flag is set on its state.

We have seen that reference change is checked on directives bindings to see if the directive is to be updated and if the view is OnPush component, it will be activated for the next CD run.

So, to make IComponent CD run. We have to change the reference of the pHuman object in PComponent before sending it to IComponent.

What does it mean, when we say to change the reference of a JS object? This simply means to change the memory address of pointed to by a variable. Let’s explain further with the example:

This is quite clear and explains the concept.

We now see for OnPush component to run on the next CD cycle, we have return new object each time the input bindings are to be updated, so we could have the ChecksEnabled flags set.

We edit the PComponent:

We create a new object and assign it to the pHuman object. Now, there is a reference change. CD will be run on the IComponent when either of the buttons are clicked.

Async Pipe

The async pipe (|) is used to subscribe and unsubscribe from Observables in our template HTML without writing an extra code.

ChangeDetectorRef class

We saw what the ChangeDetectorRef is and what it does.

Now, let’s explore its methods:

markForCheck

This method is used to enable ChecksEnabled for OnPush parent components of an OnPush component.

Let’s see where this method comes in handy.

This count won’t be re-rendered afte 10 secs, despite setTimeout being an async op. To make the count to be reflected on the DOM on each setTimeout tick. We have to call the markForCheck() method, so on the next CD run the count will be rendered.

We added the this.cd.markForCheck() inside the settimeout() call after the this.count++ increment because on each CD run, ChecksEnabled is deactivated on OnPush components. So, on each setTimeout tick, the flag is enabled so CD will run on our component.

Now, it will work as expected.

detectChanges

This is the most useful and used method in the ChangeDetectorRef class. It runs CD on the current view (and on its children).

This method runs change detection for the current component view regardless of its state.

I concur with the above statement because it jumped the gun. The CD was run directly on the component.

Note: child views with OnPush setting or on a detached state will be skipped.

detach

This method disables CD run on the current view. A detached view is not checked until attached.

The component is detached from the CD tree on initialization, the count property keeps increasing but not reflected on the DOM. Whenever we want to see the current value of count, we click the ShowCount button. It calls the detectChanges which re-renders the count property.

reattach

This is the opposite of detach. It undoes what detach did. This method enables the current view for check.

Re-attaches the previously detached view to the change detection tree.

The view was detached on initialization. Then, we reattached the view to the CD tree when the count property is 100. Then, the count property is displayed in the DOM from 100 onwards.

checkNoChanges

This method checks if no changes are made in the view’s bindings. If any change is detected, expressionChangedAfterItHasBeenCheckedError error is thrown.

Checks the change detector and its children, and throws if any changes are detected.

One last thing: to follow and understand the call of functions in Angular, the browser debugger is a very good tool. It helped me understand what happens in runtime after going through the Angular sources. The step-over, step-in, step-in buttons are very useful for navigation and the Call Stack tool comes handy when you want to track the flow of functions already executed.

Conclusion

Pheew!!! This is one huge piece. If you made it made here, congratulations and thanks for the persistence.

We covered so many concepts in Angular: CD, CD Strategies (OnPush and Default), Tree of Views, View states, the ChangeDetectorRef class and its methods going through them with precision, explaining with examples what they are, how they work and how to use them.

If you have any question regarding any concept or code in this article, feel free to comment below, email or DM me. I’d be happy to talk 😃 Thanks !!!

Helpful resources

--

--

JS | Blockchain dev | Author of “Understanding JavaScript” and “Array Methods in JavaScript” - https://app.gumroad.com/chidumennamdi 📕