DEV Community

Cover image for Migrate from `* to @` Modern Angular control flow Pattern
Leo Lanese
Leo Lanese

Posted on

Migrate from `* to @` Modern Angular control flow Pattern

With Angular 17+, the structural directives we've used for years (*ngIf, *ngFor) were replaced by built-in control flow blocks (@if, @for). While they look cleaner in the template, the real reason to migrate is what happens under the hood. The @ syntax bypasses directive instantiation entirely, compiling straight to Ivy instructions and enforcing best practices that prevent O(n) DOM updates. Let's break down why @ is faster and performs better.

βœ… Why @ is faster

πŸ”΄ The * syntax
It is just syntactic sugar for <ng-template> wrappers, which adds runtime overhead. This means Angular has to instantiate an <ng-template> element, manage a ViewContainerRef, and dynamically insert/remove views at runtime. That is real runtime overhead.

🟒 The @ syntax
No wrapper. This is the core architectural difference. @if and @for are not Directives. They don't have directive selectors, they don't go through dependency injection, and they don't use ViewContainerRef. They compiles straight to Ivy instructions, making the creation and destruction of DOM nodes significantly faster.

<!-- πŸ”΄ The OLD Angular way (Pre-Angular 17) -->
<ng-container *ngIf="companyName$ | async as name">
  <p class="vat-return-company">
    {{ name }}
    <ng-container *ngIf="vrn$ | async as vrn">
      <span>Β· VRN {{ vrn }}</span>
    </ng-container>
  </p>
</ng-container>
Enter fullscreen mode Exit fullscreen mode
<!-- 🟒 Modern Angular way -->
@if (companyName(); as name) {
  <p class="vat-return-company">
    {{ name }}
    @if (vrn(); as taxId) { <!-- Aliasing the vrn signal -->
      <span>Β· VRN {{ taxId }}</span>
      <!-- use 'taxId' multiple times without calling vrn() -->
    }
  </p>
}
Enter fullscreen mode Exit fullscreen mode

βœ… Why @ performs better

πŸ”΄ *ngFor + trackBy: trackBy was optional, leading to perfomance issues as Angular destroyed and recreated entire DOM subtrees on every change detection cycle.
🟒 Now, by making track mandatory in @for, Angular forces Devs to provide an identity function, ensuring: "**O(1) DOM updates instead of O(n)**".

In other words, instead of destroying and recreating 1,000 elements, Angular created 1 element and left the other 999 completely alone. That is O(1). This is why making track mandatory in @for is such a game-changer for Angular performance. It eliminates the O(n) trap entirely.

This single design decision probably prevents more performance bugs in Angular apps than anything else in recent versions.

<!-- πŸ”΄ COMPILER ERROR: @for loop must have a "track" expression -->
@for (item of items) {
  <div>{{ item.name }}</div>
}

<!-- βœ… COMPILES: You are forced to provide a tracking identity -->
@for (item of items; track item.id) {
  <div>{{ item.name }}</div>
}
Enter fullscreen mode Exit fullscreen mode

πŸ’― Thanks!


leolanese’s GitHub image

πŸ”˜ linkedin: @LeoLanese

πŸ”˜ Twitter: @LeoLanese

πŸ”˜ DEV.to: Blog

πŸ”˜ Questions / Suggestion / Recommendation: engineer@leolanese.com

Top comments (0)