Overview of Angular’s Change Detection operations in Ivy

Max Koretskyi
Angular In Depth
Published in
7 min readJan 4, 2023

This article is an excerpt from my Angular Deep Dive course series. The course series explains the most complicated parts of Angular. It doesn’t teach syntax or reiterate documentation; instead, it teaches fundamental concepts. All this packed with the unique information on the internal design of this mechanism. Only 2 days left before “Early Bird” option expires — price will increase from $23 to $49.

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.

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:

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

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:

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:

  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:

// 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:

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

Template function

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:

@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:

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:

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

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:

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 or on the banner below 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.

in depth knowledge we trust

--

--

Max Koretskyi
Angular In Depth

Principal Engineer at kawa.ai . Founder of indepth.dev. Big fan of software engineering, Web Platform & JavaScript. Man of Science & Philosophy.