Understanding Control Flow in Angular

Control flow refers to the mechanism that determines how code execution proceeds. Within Angular, this governs how templates behave at runtime. A typical example is using *ngIf to conditionally render sections of the template.

The Legacy Approach

In earlier versions, Angular relied on structural directives to manage template execution. These directives, including *ngIf and *ngFor, fundamentally altered the DOM's structure.

Consider these two template examples:

<button (click)="count.set(count() + 1)">Increase</button>

<div *ngIf="count()">{{ count() }}</div>

@if (double()) {
<div>{{ double() }}</div>
}

The first snippet utilizes *ngIf for conditional rendering, while the second achieves the same result with @if. Let's examine the compiled output below:

function AppComponent_Template(rf, ctx) {
    if (rf & 1) {
        elementStart(0, "button", 0);
        listener("click", function AppComponent_Template_button_click_0_listener() {
            return ctx.count.set(ctx.count() + 1);
        });
        text(1, "Increase");
        elementEnd();
        template(2, AppComponent_div_2_Template, 2, 1, "div", 1)(3, AppComponent_Conditional_3_Template, 2, 1, "div");
    }
    if (rf & 2) {
        advance(2);
        property("ngIf", ctx.count());
        advance(1);
        conditional(3, ctx.double() ? 3 : -1);
    }
},

function AppComponent_div_2_Template(rf, ctx) {
    if (rf & 1) {
        elementStart(0, "div");
        text(1);
        elementEnd();
    }
    if (rf & 2) {
        const ctx_r0 = nextContext();
        advance(1);
        textInterpolate1(" ", ctx_r0.count(), " ");
    }
}
function AppComponent_Conditional_3_Template(rf, ctx) {
    if (rf & 1) {
        elementStart(0, "div");
        text(1);
        elementEnd();
    }
    if (rf & 2) {
        const ctx_r1 = nextContext();
        advance(1);
        textInterpolate1(" ", ctx_r1.double(), " ");
    }
}

For the views themselves (AppComponent_div_2_Template and AppComponent_Conditional_3_Template), the generated code matches except for the calculations involving count and double.

Examining the creation phase (rf & 1), both *ngIf and @if are compiled into a template instruction, which is responsible for instantiating a view.

if (rf & 1) {
    ...
    template(2, AppComponent_div_2_Template, 2, 1, "div", 1)(3, AppComponent_Conditional_3_Template, 2, 1, "div");
}

This demonstrates that the modern control flow also depends on templates for regulating template execution.

Now, focusing on the update phase (rf & 2), a clear divergence appears. The *ngIf directive compiles to a property instruction, which updates a property's value—here, setting the ngIf property to the result of ctx.count(). Conversely, @if compiles to a conditional instruction. This instruction directly manages view creation or destruction, based on the truthiness of ctx.double().

if (rf & 2) {
    ...
    property("ngIf", ctx.count());
    ...
    conditional(3, ctx.double() ? 3 : -1); // 3 is the index of the view in the LView array
}

With the older ngIf, after the property is updated, Angular checks for a value change. If a change is detected, it invokes public methods such as createEmbeddedView and destroyEmbeddedView from the ViewContainerRef.

The newer conditional instruction, however, bypasses these public APIs. Instead, it directly employs internal functions like addLViewToLContainer and removeLViewFromLContainer for view creation and destruction.

Assessing Efficiency

The modern control flow exhibits superior performance compared to its predecessor. This speed advantage stems primarily from its reliance on internal APIs for view lifecycle management. Additionally, it eliminates the need to set a property and subsequently check for changes; instead, it directly acts on the condition, streamlining the entire process.

Template with *ngIf (**refreshView** takes **1.49ms**, CPU: 4x slowdown)

Template with *ngIf (refreshView takes 1.49ms, CPU: 4x slowdown)

Template with @if (**refreshView** takes **0.82ms**, CPU: 4x slowdown)

Template with @if (refreshView takes 0.82ms, CPU: 4x slowdown)

This simple example alone shows the new control flow is roughly 45% faster.

Steps for Migration

To leverage these performance gains in your projects, ensure you’ve updated your Angular version with ng update and also execute the following command:

ng g @angular/core:control-flow

This command will convert your application's templates from the legacy structural directives to the new, more efficient control flow syntax.