Overview of Angular’s Change Detection operations in Ivy

Post Editor

In this article I want to provide an overview of all operations that Angular runs during change detection in the new Ivy engine.

5 min read
1 comment
post

Overview of Angular’s Change Detection operations in Ivy

In this article I want to provide an overview of all operations that Angular runs during change detection in the new Ivy engine.

post
post
5 min read
1 comment
1 comment

A while ago I wrote an article that explored in great detail all the operations that Angular’s change detection ran during change detection. The information presented in the article became somewhat obsolete as Angular switched to a new rendering engine called Ivy in v12.

In this article I want to provide an overview of all operations that Angular runs during change detection in the new Ivy engine.

This article is an excerpt from my Angular Deep Dive course series

When Angular runs change detection for a particular component (view) it performs a number of operations. Those operations are sometimes referred to as side effects, as in a side offect of the computation logic:

In computer science, an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, which is to say if it has any observable effect other than its primary effect of returning a value to the invoker of the operation.

In Angular, the primary side effect of change detection is rendering application state to the target platform. Most often the target platfrom is a browser, application state has the form of component properties and rendering involves updating the DOM.

When checking a component Angular runs a few other operations. We can identify them by exploring the refreshView function. A bit simplified function body with my explanatory comments looks like this:

<>Copy
function refreshView(tView, lView, templateFn, context) { enterView(lView); try { if (templateFn !== null) { // update input bindings on child components // execute ngOnInit, ngOnChanges and ngDoCheck hooks // update DOM on the current component executeTemplate(tView, lView, templateFn, RenderFlags.Update, context); } // execute ngOnInit, ngOnChanges and ngDoCheck hooks // if they haven't been executed from the template function const preOrderCheckHooks = tView.preOrderCheckHooks; if (preOrderCheckHooks !== null) { executeCheckHooks(lView, preOrderCheckHooks, null); } // First mark transplanted views that are declared in this lView as needing a refresh at their // insertion points. This is needed to avoid the situation where the template is defined in this // `LView` but its declaration appears after the insertion component. markTransplantedViewsForRefresh(lView); // Refresh views added through ViewContainerRef.createEmbeddedView() refreshEmbeddedViews(lView); // Content query results must be refreshed before content hooks are called. if (tView.contentQueries !== null) { refreshContentQueries(tView, lView); } // execute content hooks (AfterContentInit, AfterContentChecked) const contentCheckHooks = tView.contentCheckHooks; if (contentCheckHooks !== null) { executeCheckHooks(lView, contentCheckHooks); } // execute logic added through @HostBinding() processHostBindingOpCodes(tView, lView); // Refresh child component views. const components = tView.components; if (components !== null) { refreshChildComponents(lView, components); } // View queries must execute after refreshing child components because a template in this view // could be inserted in a child component. If the view query executes before child component // refresh, the template might not yet be inserted. const viewQuery = tView.viewQuery; if (viewQuery !== null) { executeViewQueryFn<T>(RenderFlags.Update, viewQuery, context); } // execute view hooks (AfterViewInit, AfterViewChecked) const viewCheckHooks = tView.viewCheckHooks; if (viewCheckHooks !== null) { executeCheckHooks(lView, viewCheckHooks); } // reset the dirty state after the component is checked if (!isInCheckNoChangesPass) { lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass); } // this one is tricky :) requires its own section, we'll explore it later if (lView[FLAGS] & LViewFlags.RefreshTransplantedView) { lView[FLAGS] &= ~LViewFlags.RefreshTransplantedView; updateTransplantedViewCount(lView[PARENT] as LContainer, -1); } } finally { leaveView(); } }

We’ll take a detailed look at all those operations in the “Inside Rendering Engine” section.

For now, let’s go over the core operations run during change detection inferred from the function I showed above. Here’s the list of such operations in the order specified:

  1. executing a template function in update mode for the current view
    - checks and updates input properties on a child component/directive instance
    - execute the hooks on a child component ngOnInit, ngDoCheck and ngOnChanges if bindings changed
    - updates DOM interpolations for the current view if properties on current view component instance changed
  2. executeCheckHooks if they have not been run in the previous step
    - calls OnChanges lifecycle hook on a child component if bindings changed
    - calls ngDoCheck on a child component (OnInit is called only during first check)
  3. markTransplantedViewsForRefresh
    - find transplanted views that need to be refreshed down the Lview chain
  4. refreshEmbeddedViews
    - runs change detection for views created through ViewContainerRef APIs (mostly repeats the steps in this list)
  5. refreshContentQueries
    - updates ContentChildren query list on a child view component instance
  6. execute Content CheckHooks
    - calls AfterContentChecked lifecycle hooks on child component instance (AfterContentInit is called only during first check)
  7. processHostBindingOpCodes
    - checks and updates DOM properties on a host DOM element added through @HostBinding() syntax inside the component class
  8. refreshChildComponents
    - runs change detection for child components referenced in the current component’s template. OnPush components are skipped if they are not dirty
  9. executeViewQueryFn
    - updates ViewChildren query list on the current view component instance
  10. execute View CheckHooks (AfterViewInit, AfterViewChecked)
    - calls AfterViewChecked lifecycle hooks on child component instance (AfterViewInit is called only during first check)

Observations
Link to this section

There are few things to highlight based on the operations listed above.

Change detection for the current view is responsible for starting change detection for child views. This follows from the refreshChildComponents operation (#8 in the list above). For each child component Angular executes refreshComponent function:

<>Copy
function refreshComponent(hostLView, componentHostIdx) { const componentView = getComponentLViewByIndex(componentHostIdx, hostLView); // Only attached components that are CheckAlways // or OnPush and dirty should be refreshed if (viewAttachedToChangeDetector(componentView)) { const tView = componentView[TVIEW]; if (componentView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) { refreshView(tView, componentView, tView.template, componentView[CONTEXT]); } else if (componentView[TRANSPLANTED_VIEWS_TO_REFRESH] > 0) { // Only attached components that are CheckAlways // or OnPush and dirty should be refreshed refreshContainsDirtyView(componentView); } } }

There’s a condition that defines if a component will be checked:

<>Copy
if (viewAttachedToChangeDetector(componentView)) { ... } if (componentView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) {...}

The primary condition is that component’s changeDetectorRef has to be attached to the components tree. If it’s not attached, neither the component itself, nor its children or containing transplanted views will be checked.

If the primary condition holds, the component will be checked if it’s not OnPush or if it’s an OnPush and is dirty. There’s a logic at the end of the refreshView function that resets the dirty flag on a OnPush component:

<>Copy
// reset the dirty state after the component is checked if (!isInCheckNoChangesPass) { lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass); }

And lastly, if the component includes transplanted views they will be checked as well:

<>Copy
if (componentView[TRANSPLANTED_VIEWS_TO_REFRESH] > 0) { // Only attached components that are CheckAlways or OnPush and dirty should be refreshed refreshContainsDirtyView(componentView); }


Template function
Link to this section

The job of the executeTemplate function that Angular runs first during change detection is to execute the template function from a component’s definition. This template function is generated by the compiler for each component. For the A component:

<>Copy
@Component({ selector: 'a-cmp', template: `<b-cmp [b]="1"></b-cmp> {{updateTemplate()}}`, }) export class A { ngDoCheck() { console.log('A: ngDoCheck'); } ngAfterContentChecked() { console.log('A: ngAfterContentChecked'); } ngAfterViewChecked() { console.log('A: ngAfterViewChecked'); } updateTemplate() { console.log('A: updateTemplate'); } }

the definition looks like this:

<>Copy
import { ɵɵdefineComponent as defineComponent, ɵɵelement as element, ɵɵtext as text, ɵɵproperty as property, ɵɵadvance as advance, ɵɵtextInterpolate1 as textInterpolate1 } from '@angular/core'; export class A {} export class B {} A.ɵfac = function A_Factory(t) { return new (t || A)(); }; A.ɵcmp = defineComponent({ type: A, selectors: [["a-cmp"]], decls: 2, vars: 2, consts: [[3, "b"]], template: function A_Template(rf, ctx) { if (rf & 1) { element(0, "b-cmp", 0); text(1); } if (rf & 2) { property("b", 1); advance(1); textInterpolate1(" ", ctx.updateTemplate(), ""); } }, dependencies: function() { return [B]; }, encapsulation: 2 } );
All the functions from the import are exported with the prefix ɵɵ identifying them as private.

This template can include various instructions. In our case it includes creational instructions element and text executed during the initialization phase, and property, advance and textInterpolate1 executed during change detection phase:

<>Copy
template: function A_Template(rf, ctx) { if (rf & 1) { element(0, "b-cmp", 0); text(1); } if (rf & 2) { property("b", 1); advance(1); textInterpolate1(" ", ctx.updateTemplate(), ""); } }

Lifecyle hooks
Link to this section

It’s important to understand that most lifecycle hooks are called on the child component while Angular runs change detection for the current component. The behavior is a bit different only for the ngAfterViewChecked hook.

If you have the following components hierarchy: A -> B -> C, here is the order of hooks calls and bindings updates:

<>Copy
Entering view: A B: updateBinding B: ngOnChanges B: ngDoCheck A: updateTemplate B: ngAfterContentChecked Entering view: B С: updateBinding C: ngOnChanges С: ngDoCheck B: updateTemplate С: ngAfterContentChecked Entering view: C С: updateTemplate С: ngAfterViewChecked B: ngAfterViewChecked A: ngAfterViewChecked

That’s it for now. I keep actively adding content to the course, including some free material like the one I wrote about above. Click here to explore the course or read the article “Early bird option for the most in-depth Angular course” where I talk more about the course content and target audience.

Comments (1)

authorMaciejWWojcik
8 January 2023

great, in-depth article, as always on spot. 🤓 Thanks!

Share

About the author

author_image

Max is a self-taught software engineer that believes in fundamental knowledge and hardcore learning. He’s the founder of inDepth.dev community and one of the top users on StackOverflow (70k rep).

author_image

About the author

Max Koretskyi

Max is a self-taught software engineer that believes in fundamental knowledge and hardcore learning. He’s the founder of inDepth.dev community and one of the top users on StackOverflow (70k rep).

About the author

author_image

Max is a self-taught software engineer that believes in fundamental knowledge and hardcore learning. He’s the founder of inDepth.dev community and one of the top users on StackOverflow (70k rep).

Looking for a JS job?
Job logo
Fullstack (Angular, Node.js) Developer

Nextian Corp.

Worldwide
Remote
$84k - $107k
Job logo
Application Developer (Angular)

Karsun Solutions, LLC

Worldwide
Remote
$104k - $132k
Job logo
Angular Developer

Ryan Consulting Group

Worldwide
Remote
$77k - $80k
More jobs

Featured articles

Angularpost
17 January 202323 min read
Improve page performance and LCP with NgOptimizedImage

Explore mechanisms of NgOptimizedImage directive to improve overall page performance, targeting especially the Largest Contentful Paint (LCP) metric from Core Web Vitals. Enhance pages, make the best user experience and improve the web.