Salta al contenuto principale

3 - I Nuovi Template Angular: @if, @for, @defer

I Nuovi Template Angular: @if, @for, @defer Con Angular 17 il team ha introdotto un nuovo sistema di control flow nei template che sostituisce le storiche...

Cosa ne pensi di questo articolo?

3 - I Nuovi Template Angular: @if, @for, @defer
Angular ModernoFoto:Antonio BatinićPexels

In Breve

I Nuovi Template Angular: @if, @for, @defer Con Angular 17 il team ha introdotto un nuovo sistema di control flow nei template che sostituisce le storiche...

I Nuovi Template Angular: @if, @for, @defer

Con Angular 17 il team ha introdotto un nuovo sistema di control flow nei template che sostituisce le storiche direttive strutturali *ngIf, *ngFor e ngSwitch. La nuova sintassi con @if, @for, @switch e @defer rappresenta un cambiamento fondamentale nel modo di scrivere i template Angular: più leggibile, più performante e con funzionalità completamente nuove come il lazy loading dichiarativo dei componenti.

In questo terzo articolo della serie Angular Moderno analizzeremo in profondità ogni costrutto: dalla sintassi base ai pattern avanzati, dalla migrazione automatica alle best practices consolidate dalla community. Se stai ancora usando le direttive strutturali classiche, questa guida ti mostrera perchè è il momento di migrare.

Panoramica della Serie Angular Moderno

# Articolo Focus
1Angular SignalsReattività fine-grained
2Zoneless Change DetectionEliminare Zone.js
3Sei qui - Nuovi Template @if, @for, @deferControl flow moderno
4Standalone ComponentsArchitettura senza NgModule
5Signal FormsForm reattivi con Signals
6SSR e Incremental HydrationServer-Side Rendering
7Core Web Vitals in AngularPerformance e metriche
8Angular PWAProgressive Web App
9Dependency Injection AvanzataDI tree-shakable
10Migrare da Angular 17 a 21Guida alla migrazione

perchè una Nuova Sintassi? I Limiti delle Direttive Strutturali

Le direttive strutturali *ngIf, *ngFor e ngSwitch hanno servito Angular per quasi un decennio, ma presentano limiti strutturali che la nuova sintassi risolve completamente.

Problemi delle Direttive Strutturali Classiche

  • Microsintassi opaca: Espressioni come *ngFor="let item of items; trackBy: trackFn; let i = index; let first = first" sono difficili da leggere e ricordare
  • Nessun else-if nativo: Con *ngIf non esiste un else if: bisogna annidare più *ngIf o usare ng-template con else
  • Peso nel bundle: Le direttive strutturali sono classi Angular importate dal modulo CommonModule, che devono essere incluse nel bundle
  • Track implicito: *ngFor non obbliga a definire una funzione trackBy, causando problemi di performance silenziosi
  • Nessun lazy loading: Non esiste un meccanismo dichiarativo per caricare componenti on-demand direttamente dal template
  • Verbosita: Per un semplice if/else servono ng-template con variabili template reference, aumentando il boilerplate

Guardiamo un esempio concreto che mostra la differenza di leggibilità.

*ngIf con else: la vecchia sintassi

<!-- Vecchia sintassi: verbose e frammentata -->
<div *ngIf="user; else loadingTpl">
  <h2>{{ user.name }}</h2>
  <div *ngIf="user.role === 'admin'; else userTpl">
    <span class="badge admin">Amministratore</span>
  </div>
  <ng-template #userTpl>
    <span class="badge user">Utente</span>
  </ng-template>
</div>
<ng-template #loadingTpl>
  <div class="spinner">Caricamento...</div>
</ng-template>
    
@if con @else: la nuova sintassi

<!-- Nuova sintassi: chiara e lineare -->
@if (user) {
  <h2>{{ user.name }}</h2>
  @if (user.role === 'admin') {
    <span class="badge admin">Amministratore</span>
  } @else {
    <span class="badge user">Utente</span>
  }
} @else {
  <div class="spinner">Caricamento...</div>
}
    

Vantaggi della Nuova Sintassi in Sintesi

Aspetto Direttive Strutturali Nuovo Control Flow
Importazioni necessarie CommonModule o import singoli Nessuna (built-in nel compiler)
Else-if Non supportato nativamente @else if nativo
Track obbligatorio No (facoltativo) Si (obbligatorio in @for)
Lazy loading Non disponibile @defer con trigger dichiarativi
Bundle size Direttive incluse nel bundle Zero overhead (compilato)
Type checking Limitato Completo
Leggibilita Microsintassi complessa Simile a JavaScript/TypeScript

@if e @else: Rendering Condizionale Moderno

Il blocco @if sostituisce *ngIf con una sintassi che ricorda le strutture di controllo di JavaScript. Supporta nativamente @else if e @else, eliminando la necessità di ng-template.

Sintassi Base

@if: sintassi base e @else

<!-- Condizione semplice -->
@if (isLoggedIn()) {
  <app-dashboard />
}

<!-- If con else -->
@if (user()) {
  <h2>Benvenuto, {{ user().name }}</h2>
} @else {
  <app-login-form />
}

<!-- If / else if / else -->
@if (status() === 'loading') {
  <div class="spinner">Caricamento in corso...</div>
} @else if (status() === 'error') {
  <div class="error">Si è verificato un errore</div>
} @else if (status() === 'empty') {
  <div class="empty">Nessun risultato trovato</div>
} @else {
  <app-data-table [data]="data()" />
}
    

Alias con la keyword "as"

Una funzionalità potente di @if e la possibilità di assegnare un alias al risultato della condizione. Questo è particolarmente utile quando si lavora con Signal o Observable che restituiscono valori nullable.

@if con alias (as)

<!-- Salva il risultato della condizione in una variabile locale -->
@if (currentUser(); as user) {
  <div class="profile">
    <h3>{{ user.name }}</h3>
    <p>Email: {{ user.email }}</p>
    <p>Ruolo: {{ user.role }}</p>
  </div>
}

<!-- Utile con pipe asincrone o trasformazioni -->
@if (users() | filterActive; as activeUsers) {
  <p>Utenti attivi: {{ activeUsers.length }}</p>
}
    

Differenze chiave rispetto a *ngIf

  • @if non richiede nessuna importazione (non è una direttiva, è parte del compiler)
  • @else if è supportato nativamente, eliminando la necessità di ng-template annidati
  • La sintassi as funziona come aliasing del valore truthy della condizione
  • Il type narrowing funziona correttamente: dentro il blocco @if il tipo è già ristretto
  • Non serve più il CommonModule o l'import diretto di NgIf

@for con track: Iterazione Performante e Obbligatoria

Il blocco @for sostituisce *ngFor introducendo un cambiamento fondamentale: l'espressione track è obbligatoria. Questa scelta di design forza gli sviluppatori a definire come Angular identifica univocamente ogni elemento della lista, prevenendo problemi di performance che con *ngFor passavano inosservati.

Sintassi Base

@for: sintassi con track obbligatorio

<!-- Iterazione base con track per id -->
@for (product of products(); track product.id) {
  <div class="product-card">
    <h3>{{ product.name }}</h3>
    <p>{{ product.price | currency:'EUR' }}</p>
  </div>
}

<!-- Con variabili implicite -->
@for (item of items(); track item.id; let i = $index; let isFirst = $first; let isLast = $last) {
  <div class="item" [class.first]="isFirst" [class.last]="isLast">
    <span class="index">{{ i + 1 }}.</span>
    <span>{{ item.name }}</span>
  </div>
}

<!-- Track per indice (per liste senza id univoco) -->
@for (label of labels(); track $index) {
  <span class="tag">{{ label }}</span>
}
    

Variabili Implicite Disponibili in @for

Variabile Tipo Descrizione
$indexnumberIndice corrente (0-based)
$firstbooleantrue se e il primo elemento
$lastbooleantrue se e l'ultimo elemento
$evenbooleantrue se l'indice e pari
$oddbooleantrue se l'indice e dispari
$countnumberNumero totale di elementi

perchè track e Obbligatorio

Quando Angular aggiorna una lista, deve decidere quali elementi del DOM ricreare, quali spostare e quali mantenere invariati. L'espressione track fornisce ad Angular un'identità stabile per ogni elemento, permettendo aggiornamenti ottimali.

Confronto performance: track corretto vs $index

// Componente con lista aggiornata frequentemente
@Component({
  selector: 'app-live-feed',
  standalone: true,
  template: `
    <!-- CORRETTO: track per identità unica -->
    <!-- Quando la lista cambia, Angular riusa i nodi DOM esistenti -->
    @for (post of posts(); track post.id) {
      <app-post-card [post]="post" />
    }

    <!-- SCONSIGLIATO: track per indice -->
    <!-- Se un elemento viene aggiunto in cima, TUTTI i nodi vengono ricreati -->
    @for (post of posts(); track $index) {
      <app-post-card [post]="post" />
    }
  `
})
export class LiveFeedComponent {
  posts = signal<Post[]>([]);

  addNewPost(post: Post) {
    // Con track post.id: solo 1 nodo DOM creato
    // Con track $index: N nodi DOM ricreati
    this.posts.update(list => [post, ...list]);
  }
}
    

Quando usare $index come track

Usa track $index solo quando gli elementi della lista non hanno un'identità stabile (ad esempio, un array di stringhe semplici). In tutti gli altri casi, preferisci sempre una proprietà univoca come id. Usare $index su liste che cambiano frequentemente è una delle cause principali di performance scadenti nelle applicazioni Angular.

@switch: Selezione Multipla Pulita

Il blocco @switch sostituisce la combinazione di direttive ngSwitch, *ngSwitchCase e *ngSwitchDefault con una sintassi molto più naturale e leggibile.

Confronto ngSwitch vs @switch

<!-- PRIMA: ngSwitch (verboso, richiede contenitore) -->
<div [ngSwitch]="orderStatus">
  <div *ngSwitchCase="'pending'">
    <span class="badge warning">In Attesa</span>
  </div>
  <div *ngSwitchCase="'processing'">
    <span class="badge info">In Lavorazione</span>
  </div>
  <div *ngSwitchCase="'shipped'">
    <span class="badge success">Spedito</span>
  </div>
  <div *ngSwitchCase="'delivered'">
    <span class="badge success">Consegnato</span>
  </div>
  <div *ngSwitchDefault>
    <span class="badge">Sconosciuto</span>
  </div>
</div>

<!-- DOPO: @switch (pulito, senza contenitore) -->
@switch (orderStatus()) {
  @case ('pending') {
    <span class="badge warning">In Attesa</span>
  }
  @case ('processing') {
    <span class="badge info">In Lavorazione</span>
  }
  @case ('shipped') {
    <span class="badge success">Spedito</span>
  }
  @case ('delivered') {
    <span class="badge success">Consegnato</span>
  }
  @default {
    <span class="badge">Sconosciuto</span>
  }
}
    

Note su @switch

  • @switch usa il confronto stretto (===), identico a JavaScript
  • Non richiede un elemento contenitore: il confronto è direttamente sull'espressione
  • @default è opzionale ma consigliato per gestire casi imprevisti
  • Non supporta il fall-through: ogni @case è isolato (comportamento desiderabile)
  • A differenza di ngSwitch, non servono importazioni dal CommonModule

@defer: Lazy Loading Dichiarativo

@defer è la vera rivoluzione introdotta con la nuova sintassi dei template. Permette di caricare componenti, direttive e pipe on-demand direttamente dal template, senza necessità di codice TypeScript per il lazy loading. Il compilatore Angular automaticamente separa il codice referenziato dentro @defer in chunk separati, scaricati solo quando il trigger specificato si attiva.

Sintassi Base e Blocchi Associati

@defer: struttura completa con placeholder, loading ed error

@defer (on viewport) {
  <!-- Contenuto caricato lazily -->
  <app-heavy-chart [data]="chartData()" />
} @placeholder {
  <!-- Mostrato prima che il caricamento inizi -->
  <div class="chart-placeholder">
    <p>Il grafico apparira qui</p>
  </div>
} @loading (after 150ms; minimum 300ms) {
  <!-- Mostrato durante il caricamento del chunk -->
  <div class="spinner">Caricamento grafico...</div>
} @error {
  <!-- Mostrato se il caricamento fallisce -->
  <div class="error">Impossibile caricare il grafico</div>
}
    

Blocchi Associati a @defer

Blocco Scopo Opzioni
@placeholder Contenuto mostrato prima che il trigger si attivi minimum: tempo minimo di visualizzazione
@loading Contenuto mostrato durante il download del chunk after: ritardo prima di mostrarlo; minimum: durata minima
@error Contenuto mostrato se il caricamento fallisce Nessuna opzione

Trigger Disponibili

@defer supporta diversi trigger che determinano quando iniziare il caricamento del contenuto lazy. Possono essere combinati con l'operatore logico per creare condizioni di caricamento sofisticate.

Tutti i trigger di @defer

<!-- 1. on viewport: carica quando l'elemento entra nel viewport -->
@defer (on viewport) {
  <app-comments [postId]="postId()" />
} @placeholder {
  <div style="height: 200px">Scorri per vedere i commenti</div>
}

<!-- 2. on idle: carica quando il browser è inattivo -->
@defer (on idle) {
  <app-analytics-widget />
}

<!-- 3. on interaction: carica al click/focus su un elemento -->
<button #loadBtn>Mostra dettagli</button>
@defer (on interaction(loadBtn)) {
  <app-product-details [id]="productId()" />
} @placeholder {
  <p>Clicca il pulsante per vedere i dettagli</p>
}

<!-- 4. on hover: carica quando il mouse passa sopra un elemento -->
<div #previewArea>Passa il mouse per l'anteprima</div>
@defer (on hover(previewArea)) {
  <app-image-preview [url]="imageUrl()" />
}

<!-- 5. on timer: carica dopo un intervallo di tempo -->
@defer (on timer(5s)) {
  <app-newsletter-popup />
}

<!-- 6. when: carica quando una condizione diventa true -->
@defer (when isAuthenticated()) {
  <app-admin-panel />
} @placeholder {
  <p>Effettua il login per accedere al pannello admin</p>
}

<!-- 7. Combinazione di trigger -->
@defer (on viewport; on timer(10s)) {
  <app-sidebar-ads />
} @placeholder {
  <div class="ad-placeholder"></div>
}
    

Riepilogo dei Trigger @defer

Trigger Attivazione Caso d'uso tipico
on viewport L'elemento placeholder entra nel viewport Contenuti below-the-fold, commenti, footer
on idle Il browser è inattivo (requestIdleCallback) Widget non critici, analytics
on interaction Click o focus su un elemento specifico Pannelli espandibili, dettagli on-demand
on hover Mouse hover su un elemento specifico Tooltip complessi, anteprime
on timer(Ns) Dopo N secondi/millisecondi Popup, contenuti ritardati
when condizione Quando l'espressione diventa truthy Contenuti basati su stato (login, flag)

Prefetching Intelligente

@defer supporta anche il prefetching: puoi iniziare a scaricare il chunk prima che il trigger di rendering si attivi. Questo migliora l'esperienza utente perchè il contenuto è già pronto quando serve.

Prefetching con @defer

<!-- Prefetch on idle, render on viewport -->
<!-- Il browser scarica il chunk quando è inattivo, -->
<!-- ma lo rende visibile solo quando entra nel viewport -->
@defer (on viewport; prefetch on idle) {
  <app-heavy-data-grid [data]="gridData()" />
} @placeholder {
  <div class="grid-skeleton">
    <div class="skeleton-row"></div>
    <div class="skeleton-row"></div>
    <div class="skeleton-row"></div>
  </div>
}

<!-- Prefetch on hover, render on interaction -->
<!-- L'utente passa il mouse e il chunk si scarica, -->
<!-- quando clicca il contenuto appare istantaneamente -->
<button #editBtn>Modifica Profilo</button>
@defer (on interaction(editBtn); prefetch on hover(editBtn)) {
  <app-profile-editor [user]="currentUser()" />
} @placeholder {
  <p>Clicca per modificare il profilo</p>
}
    

@empty: Gestire Liste Vuote con Eleganza

Il blocco @empty è un compagno di @for che permette di mostrare contenuto alternativo quando la collezione iterata è vuota. Prima della nuova sintassi, questo richiedeva un *ngIf separato per controllare la lunghezza dell'array.

@for con @empty: gestione stato vuoto

<!-- PRIMA: due direttive separate -->
<div *ngIf="notifications.length === 0" class="empty-state">
  <p>Nessuna notifica</p>
</div>
<div *ngFor="let notification of notifications">
  {{ notification.message }}
</div>

<!-- DOPO: integrato nel control flow -->
@for (notification of notifications(); track notification.id) {
  <div class="notification" [class]="notification.type">
    <span class="icon">{{ notification.icon }}</span>
    <p>{{ notification.message }}</p>
    <time>{{ notification.date | date:'short' }}</time>
  </div>
} @empty {
  <div class="empty-state">
    <img src="/assets/no-notifications.svg" alt="Nessuna notifica" />
    <h3>Tutto in ordine!</h3>
    <p>Non hai notifiche al momento.</p>
  </div>
}
    

Vantaggi di @empty

  • Elimina la duplicazione del check array.length === 0
  • Il blocco @empty viene renderizzato solo quando la collezione ha zero elementi
  • Migliora la leggibilità raggruppando logica di iterazione e stato vuoto
  • Funziona con qualsiasi iterabile: array, Signal di array, risultati filtrati

Migrazione Automatica con Schematics

Il team Angular fornisce uno schematic ufficiale che migra automaticamente i template dalla vecchia sintassi alla nuova. Questo strumento analizza ogni template, converte le direttive strutturali e genera codice pulito.

Eseguire la migrazione automatica

# Migrazione automatica del control flow
ng generate @angular/core:control-flow-migration

# Il comando:
# 1. Analizza tutti i template dell'applicazione
# 2. Converte *ngIf -> @if
# 3. Converte *ngFor -> @for (con track $index come default)
# 4. Converte ngSwitch -> @switch
# 5. Rimuove le importazioni di CommonModule dove non più necessarie

# Per un singolo componente
ng generate @angular/core:control-flow-migration --path=src/app/components/my-component

# Per una dry run (anteprima senza modificare i file)
ng generate @angular/core:control-flow-migration --dry-run
    

Attenzione alla Migrazione Automatica

  • Track default: Lo schematic inserisce track $index come default per i @for. Dopo la migrazione, rivedi ogni occorrenza e sostituisci con la proprietà univoca corretta (es. track item.id)
  • Formattazione: La migrazione potrebbe alterare l'indentazione. Esegui il formatter del progetto dopo la migrazione
  • Template complessi: Template con direttive strutturali annidate e complesse potrebbero richiedere revisione manuale
  • Test: Esegui tutti i test dopo la migrazione per verificare che il comportamento sia invariato
  • Commit prima: Committa il codice prima di eseguire la migrazione, cosi puoi facilmente confrontare le differenze
Esempio di output della migrazione

<!-- PRIMA della migrazione -->
<div *ngIf="users$ | async as users; else loading">
  <table>
    <tr *ngFor="let user of users; trackBy: trackByUserId; let i = index">
      <td>{{ i + 1 }}</td>
      <td>{{ user.name }}</td>
      <td [ngSwitch]="user.status">
        <span *ngSwitchCase="'active'" class="green">Attivo</span>
        <span *ngSwitchCase="'inactive'" class="red">Inattivo</span>
        <span *ngSwitchDefault class="gray">N/D</span>
      </td>
    </tr>
  </table>
</div>
<ng-template #loading>
  <div class="spinner">Caricamento...</div>
</ng-template>

<!-- DOPO la migrazione automatica -->
@if (users$ | async; as users) {
  <table>
    @for (user of users; track user.id; let i = $index) {
      <tr>
        <td>{{ i + 1 }}</td>
        <td>{{ user.name }}</td>
        <td>
          @switch (user.status) {
            @case ('active') {
              <span class="green">Attivo</span>
            }
            @case ('inactive') {
              <span class="red">Inattivo</span>
            }
            @default {
              <span class="gray">N/D</span>
            }
          }
        </td>
      </tr>
    }
  </table>
} @else {
  <div class="spinner">Caricamento...</div>
}
    

Pattern Avanzati: Combinare il Control Flow

La vera potenza del nuovo control flow emerge quando si combinano i vari costrutti per risolvere scenari reali. Vediamo alcuni pattern avanzati utilizzati comunemente in produzione.

Pattern 1: Dashboard con Lazy Loading per Sezioni

Dashboard con @defer per ogni widget

<div class="dashboard">
  <!-- Header sempre visibile -->
  <header>
    <h1>Dashboard</h1>
    @if (user(); as u) {
      <span>Benvenuto, {{ u.name }}</span>
    }
  </header>

  <!-- KPI caricati immediatamente quando idle -->
  @defer (on idle) {
    <app-kpi-cards [metrics]="kpiData()" />
  } @placeholder {
    <div class="kpi-skeleton">
      @for (i of [1,2,3,4]; track $index) {
        <div class="skeleton-card"></div>
      }
    </div>
  }

  <!-- Grafico caricato quando entra nel viewport -->
  @defer (on viewport; prefetch on idle) {
    <app-revenue-chart [data]="revenueData()" />
  } @placeholder {
    <div class="chart-placeholder" style="height: 400px">
      <p>Grafico ricavi</p>
    </div>
  } @loading (after 200ms) {
    <div class="spinner">Caricamento grafico...</div>
  }

  <!-- Tabella dati caricata su richiesta -->
  <button #showTable>Mostra dettagli</button>
  @defer (on interaction(showTable); prefetch on hover(showTable)) {
    @if (tableData(); as data) {
      @for (row of data; track row.id) {
        <app-data-row [row]="row" />
      } @empty {
        <p>Nessun dato disponibile per il periodo selezionato</p>
      }
    }
  } @placeholder {
    <p>Clicca per visualizzare i dettagli</p>
  }
</div>
    

Pattern 2: Lista con Filtri, Ordinamento e Stato Vuoto

Lista completa con control flow annidato

<div class="product-list">
  <!-- Filtri -->
  <div class="filters">
    @for (category of categories(); track category.id) {
      <button
        [class.active]="selectedCategory() === category.id"
        (click)="selectCategory(category.id)">
        {{ category.name }}
        <span class="count">({{ category.count }})</span>
      </button>
    }
  </div>

  <!-- Stato di caricamento -->
  @if (isLoading()) {
    <div class="loading">
      @for (i of [1,2,3]; track $index) {
        <div class="product-skeleton"></div>
      }
    </div>
  } @else {
    <!-- Lista prodotti -->
    @for (product of filteredProducts(); track product.id) {
      <div class="product-card">
        <h3>{{ product.name }}</h3>
        <p>{{ product.description }}</p>

        @switch (product.availability) {
          @case ('in_stock') {
            <span class="badge success">Disponibile</span>
          }
          @case ('low_stock') {
            <span class="badge warning">Ultime scorte ({{ product.stock }})</span>
          }
          @case ('out_of_stock') {
            <span class="badge danger">Esaurito</span>
          }
        }

        <div class="price">{{ product.price | currency:'EUR' }}</div>
      </div>
    } @empty {
      <div class="empty-state">
        <h3>Nessun prodotto trovato</h3>
        <p>Prova a modificare i filtri di ricerca</p>
        <button (click)="resetFilters()">Resetta filtri</button>
      </div>
    }
  }
</div>
    

Pattern 3: @defer Condizionale con Permessi Utente

Lazy loading basato su ruolo utente

@Component({
  selector: 'app-settings-page',
  standalone: true,
  template: `
    <h1>Impostazioni</h1>

    <!-- Sezione profilo: sempre disponibile -->
    <app-profile-settings />

    <!-- Pannello admin: caricato solo se l'utente è admin -->
    @defer (when isAdmin()) {
      <app-admin-settings />
    } @placeholder {
      @if (!isAdmin()) {
        <div class="restricted">
          <p>Accesso riservato agli amministratori</p>
        </div>
      }
    }

    <!-- Impostazioni avanzate: defer + condizione -->
    @if (showAdvanced()) {
      @defer (on idle) {
        <app-advanced-settings />
      } @loading {
        <div class="spinner">Caricamento impostazioni avanzate...</div>
      }
    }
  `
})
export class SettingsPageComponent {
  private authService = inject(AuthService);

  isAdmin = computed(() =>
    this.authService.currentUser()?.role === 'admin'
  );
  showAdvanced = signal(false);
}
    

Impatto sulle Performance

Il nuovo control flow non è solo più leggibile: ha vantaggi concreti e misurabili sulle performance dell'applicazione. Il compilatore Angular genera codice ottimizzato che elimina l'overhead runtime delle direttive strutturali.

Benchmark: Nuovo Control Flow vs Direttive Strutturali

Metrica Direttive (*ngIf, *ngFor) Nuovo Control Flow Miglioramento
Bundle size (CommonModule) ~12KB (minified) 0KB (built-in compiler) -100%
Rendering lista 1000 elementi ~45ms ~38ms -15%
Re-rendering con modifica singola ~8ms ~3ms -62%
Memory footprint per @for Direttiva + template ref + embedded view Solo istruzioni compilate Significativo
Lazy loading con @defer Non disponibile (solo route-level) A livello di componente/template Nuova funzionalità

perchè il Nuovo Control Flow e Più Veloce

  • Compilazione diretta: I blocchi @if e @for vengono compilati in istruzioni JavaScript ottimizzate dal compilatore Angular, senza l'overhead di creare istanze di direttive a runtime
  • Nessuna ViewContainerRef: Le direttive strutturali usano ViewContainerRef per creare/distruggere viste embedded. Il nuovo control flow gestisce il DOM direttamente
  • Track ottimizzato: L'algoritmo di diffing del @for è stato riscritto da zero per essere più efficiente rispetto a DefaultIterableDiffer
  • Tree shaking: Non essendo direttive, non occupano spazio nel bundle se non usate. E anche se usate, il codice generato è minimale
  • @defer chunk splitting: Il compilatore crea automaticamente chunk separati per il contenuto lazy, riducendo il payload iniziale

Best Practices per il Nuovo Control Flow

Dopo mesi di utilizzo in produzione dalla community Angular, si sono consolidate best practices chiare per sfruttare al meglio il nuovo control flow.

Le 10 Best Practices del Nuovo Control Flow

  1. Usa sempre una proprietà univoca nel track: Evita track $index a meno che la lista non contenga valori primitivi senza identità. Preferisci track item.id
  2. Sfrutta @empty per stati vuoti: Ogni @for che mostra dati all'utente dovrebbe avere un blocco @empty con un messaggio utile
  3. Usa @defer per contenuti below-the-fold: Tutto ciò che non è visibile al primo rendering dovrebbe essere wrappato in @defer (on viewport)
  4. Combina prefetch e trigger: Usa prefetch on idle per pre-scaricare chunk durante i momenti di inattivita del browser
  5. Preferisci @else if a @switch per pochi casi: Se hai 2-3 condizioni, @if / @else if è più leggibile di @switch
  6. Fornisci sempre un @placeholder per @defer: L'utente deve capire che un contenuto sta per apparire; un placeholder vuoto confonde
  7. Usa minimum e after in @loading: Evita il "flash" di loading per contenuti che si caricano rapidamente con @loading (after 150ms; minimum 300ms)
  8. Non annidare troppo: Più di 3 livelli di control flow annidati rendono il template illeggibile. Estrai sotto-componenti
  9. Migra completamente: Non mischiare vecchia e nuova sintassi nello stesso template; confonde e complica la manutenzione
  10. Rivedi dopo la migrazione automatica: Lo schematic usa track $index come default. Rivedi ogni @for e assegna la track expression corretta

Anti-pattern da Evitare

Errori comuni con il nuovo control flow

<!-- ERRORE 1: Track per $index su liste dinamiche -->
<!-- Se un elemento viene inserito in testa, TUTTI i nodi DOM vengono ricreati -->
@for (user of users(); track $index) {
  <app-user-card [user]="user" />  <!-- Ricreato inutilmente! -->
}
<!-- FIX: track user.id -->

<!-- ERRORE 2: @defer senza placeholder -->
<!-- L'utente vede uno spazio vuoto senza capire cosa succede -->
@defer (on viewport) {
  <app-comments />
}
<!-- FIX: aggiungi @placeholder con indicazione visiva -->

<!-- ERRORE 3: @defer per contenuti critici above-the-fold -->
<!-- Il contenuto principale non deve essere lazy -->
@defer (on idle) {
  <app-hero-section />  <!-- MAI! Il hero deve essere immediato -->
}

<!-- ERRORE 4: Annidamento eccessivo -->
@if (condition1()) {
  @for (item of items(); track item.id) {
    @if (item.visible) {
      @switch (item.type) {
        @case ('A') {
          @for (sub of item.children; track sub.id) {
            <!-- Troppo annidato! Estrai un componente -->
          }
        }
      }
    }
  }
}
<!-- FIX: estrai sotto-componenti per ridurre l'annidamento -->
    

Checklist per la Migrazione

Checklist Migrazione al Nuovo Control Flow

  1. Committa il codice corrente prima della migrazione
  2. Esegui ng generate @angular/core:control-flow-migration --dry-run
  3. Rivedi i cambiamenti proposti
  4. Esegui la migrazione reale
  5. Sostituisci tutti i track $index con proprietà univoche
  6. Aggiungi blocchi @empty dove appropriato
  7. Valuta dove aggiungere @defer per lazy loading
  8. Rimuovi le importazioni di CommonModule, NgIf, NgFor, NgSwitch
  9. Esegui il formatter per uniformare l'indentazione
  10. Esegui tutti i test (unit + e2e)
  11. Verifica visivamente ogni pagina

Riepilogo e Prossimi Passi

Il nuovo control flow di Angular rappresenta un salto qualitativo nella developer experience del framework. La sintassi con @if, @for, @switch e @defer non è solo più leggibile: produce codice più performante, abilita il lazy loading dichiarativo e forza best practices come il track obbligatorio.

Concetti Chiave di Questo Articolo

  • @if / @else if / @else: Sostituisce *ngIf con supporto nativo per else-if e alias tramite as
  • @for con track: Sostituisce *ngFor con track obbligatorio per performance garantite. Include variabili implicite ($index, $first, $last, $even, $odd, $count)
  • @switch / @case / @default: Sostituisce ngSwitch con sintassi pulita e confronto stretto
  • @defer: Lazy loading dichiarativo con trigger (viewport, idle, interaction, hover, timer, when) e prefetching
  • @empty: Gestione elegante delle liste vuote, integrata direttamente nel @for
  • Migrazione: Lo schematic @angular/core:control-flow-migration automatizza la conversione
  • Performance: Zero overhead di bundle (compilato, non direttive), diffing ottimizzato, chunk splitting con @defer

Nel prossimo articolo della serie esploreremo gli Standalone Components, l'architettura che elimina completamente gli NgModule da Angular, semplificando la struttura dell'applicazione e migliorando il tree-shaking e il lazy loading a livello di route.

Domande frequenti

Qual è il motivo per cui le direttive strutturali non sono più utilizzate in Angular?

Le direttive strutturali presentano limiti strutturali che la nuova sintassi risolve completamente.

Come funziona l'alias con la keyword "as" nella nuova sintassi di Angular?

L'alias consente di assegnare un nome al risultato della condizione, utilissimo quando si lavora con Signal o Observable che restituiscono valori nullable.

Perché è obbligatorio specificare l'espressione track nella nuova sintassi di Angular?

L'espressione track fornisce ad Angular un'identità stabile per ogni elemento, permettendo aggiornamenti ottimali della lista e prevenendo problemi di performance.

Qual è il vantaggio principale del lazy loading dichiarativo introdotto con la nuova sintassi dei template?

Il lazy loading dichiarativo consente di caricare componenti, direttive e pipe on-demand direttamente dal template, senza necessità di codice TypeScript per il lazy loading.

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti