Angular Standalone Components: migrazione e best practice per Angular 22

Guida completa alla migrazione di applicazioni Angular dai NgModule agli standalone component. Copre la migrazione ufficiale CLI in 3 passaggi, i selectorless component, OnPush di default e lazy loading con loadComponent in Angular 22.

Architettura della migrazione agli standalone component di Angular: passaggio dall'albero NgModule a quello standalone

Gli standalone component di Angular eliminano la necessità dei NgModule, riducono il boilerplate e abilitano un lazy loading a grana fine in tutta l'applicazione. Da quando Angular 19 ha reso lo standalone il default e Angular 22 ha introdotto i selectorless component come stabili, migrare codebase legacy basate su moduli è diventato sia semplice sia ad alto impatto.

Punto chiave

Lo schematic ufficiale dell'Angular CLI gestisce automaticamente la maggior parte della migrazione in tre passaggi. Una tipica applicazione enterprise può completare la conversione in un singolo sprint, con bundle che si riducono del 30-50 % grazie al lazy loading per componente.

NgModule vs standalone component: cosa è cambiato

I NgModule hanno funzionato come contesto di compilazione per i componenti fin da Angular 2. Ogni componente, direttiva e pipe doveva essere dichiarato in esattamente un modulo, e la condivisione di funzionalità richiedeva import ed export di moduli orchestrati con cura. Ne risultava un forte accoppiamento tra feature non correlate e un tree-shaking difficoltoso.

Gli standalone component ribaltano questo modello. Ogni componente dichiara le proprie dipendenze direttamente nell'array imports del decoratore @Component. Niente registrazione nei moduli, niente moduli condivisi, niente barrel export di mezza applicazione. A partire da Angular 22, questo pattern è ulteriormente semplificato dai selectorless component, che permettono import diretti senza selettori stringa.

hero-list.component.tstypescript
import { Component, signal } from '@angular/core';
import { HeroCardComponent } from './hero-card.component';
import { SearchPipe } from '../pipes/search.pipe';

@Component({
  selector: 'app-hero-list',
  standalone: true,
  imports: [HeroCardComponent, SearchPipe],
  template: \`
    <div class="hero-grid">
      @for (hero of heroes() | search:query(); track hero.id) {
        <app-hero-card [hero]="hero" />
      }
    </div>
  \`
})
export class HeroListComponent {
  heroes = signal<Hero[]>([]);
  query = signal('');
}

L'array imports sostituisce l'intero grafo delle dipendenze dei NgModule. Il bundler vede esattamente quali componenti, pipe e direttive servono a ciascun file, abilitando un tree-shaking preciso. Il CommonModule non è più necessario in Angular 22 poiché la sintassi di control flow (@for, @if) è integrata nel compilatore.

Il processo di migrazione CLI in 3 passaggi

Angular fornisce uno schematic automatizzato che gestisce la migrazione in tre passaggi sequenziali. Ogni step si basa sul precedente, e tra un passaggio e l'altro il progetto deve compilare senza errori.

Passaggio 1: convertire le dichiarazioni in standalone

Il primo passaggio analizza ogni componente, direttiva e pipe del progetto, aggiunge standalone: true e sposta gli import necessari dal NgModule padre nell'array imports di ciascun componente.

bash
# Step 1: Convert all declarations to standalone
ng generate @angular/core:standalone --path=src/app

Alla richiesta, selezionare "Convert all components, directives and pipes to standalone". Lo schematic usa l'analisi statica per risolvere le dipendenze, quindi qualunque componente con metadata non analizzabili a build time verrà saltato con un warning.

Passaggio 2: rimuovere i NgModule non necessari

Con tutte le dichiarazioni ora standalone, molti NgModule diventano gusci vuoti. Questo passaggio identifica i moduli che si limitavano a riesportare dichiarazioni standalone e li rimuove.

bash
# Step 2: Remove empty NgModules
ng generate @angular/core:standalone --path=src/app

Selezionare "Remove unnecessary NgModule classes". I moduli che contengono ancora provider, configurazioni di rotte o sono importati da più moduli vengono mantenuti con un commento TODO per la revisione manuale.

Requisito Angular 22

Angular 22 richiede TypeScript 6 e Node.js 26. Le versioni precedenti non sono supportate. Eseguire ng version per verificare la compatibilità prima di iniziare la migrazione.

Passaggio 3: passare al bootstrap standalone

L'ultimo passaggio sostituisce il NgModule root con l'API bootstrapApplication di Angular e converte il componente root in standalone.

main.ts (after migration)typescript
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));
app.config.tstypescript
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor]))
  ]
};

Il pattern ApplicationConfig sostituisce gli array providers e imports del modulo root. Tutte le funzioni provider (provideRouter, provideHttpClient, provideAnimations) funzionano direttamente senza wrapper modulari.

Angular 22: OnPush di default e selectorless component

Angular 22 ha introdotto due cambiamenti che influenzano lo sviluppo con standalone component: OnPush come strategia di change detection predefinita e i selectorless component che raggiungono lo stato stabile.

OnPush di default

I nuovi componenti generati in Angular 22 usano ChangeDetectionStrategy.OnPush di default. Questo favorisce la reattività basata su signal rispetto ai controlli attivati da Zone.js. Durante la migrazione, lo schematic aggiunge esplicitamente changeDetection: ChangeDetectionStrategy.Default ai componenti esistenti per mantenere la retrocompatibilità.

typescript
// Angular 22: OnPush is implicit for new components
@Component({
  selector: 'app-dashboard',
  standalone: true,
  // changeDetection: ChangeDetectionStrategy.OnPush is now the default
  template: \`<h1>{{ title() }}</h1>\`
})
export class DashboardComponent {
  title = signal('Dashboard');
}

Per le applicazioni migrate, si consiglia di adottare OnPush in modo incrementale sostituendo le proprietà di classe con signal. Questo si allinea con l'architettura signal-first promossa da Angular 22.

Selectorless component (stabili)

I selectorless component permettono di importare componenti direttamente nei template senza definire selettori stringa. Questo elimina i conflitti di naming dei selettori nelle grandi codebase e fornisce una migliore type safety.

typescript
// Traditional approach with selector
@Component({
  selector: 'app-user-card',
  standalone: true,
  template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
  user = input.required<User>();
}

// Selectorless approach (Angular 22+)
@Component({
  standalone: true,
  template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
  user = input.required<User>();
}

Nei template, i selectorless component vengono usati tramite il loro nome di classe invece di un tag selettore personalizzato. Il compilatore verifica che l'import esista, intercettando gli errori a build time invece che a runtime.

Migrazione del routing: dai moduli a loadComponent

I moduli di routing richiedono attenzione manuale perché lo schematic non converte automaticamente gli import di loadChildren di moduli in loadComponent o in loadChildren a livello di rotta con rotte standalone.

Il pattern legacy caricava interi moduli di feature:

app.routes.ts (before)typescript
const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module')
      .then(m => m.DashboardModule)
  }
];

L'equivalente standalone carica direttamente singoli componenti o file di rotte:

app.routes.ts (after)typescript
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/dashboard.component')
      .then(c => c.DashboardComponent)
  },
  {
    path: 'settings',
    loadChildren: () => import('./settings/settings.routes')
      .then(r => r.settingsRoutes)
  }
];
settings/settings.routes.tstypescript
import { Routes } from '@angular/router';

export const settingsRoutes: Routes = [
  {
    path: '',
    loadComponent: () => import('./settings.component')
      .then(c => c.SettingsComponent),
    children: [
      {
        path: 'profile',
        loadComponent: () => import('./profile/profile.component')
          .then(c => c.ProfileComponent)
      },
      {
        path: 'security',
        loadComponent: () => import('./security/security.component')
          .then(c => c.SecurityComponent)
      }
    ]
  }
];

loadComponent carica in lazy un singolo componente. loadChildren con un file di rotte carica in lazy un'intera area di feature. Entrambi producono chunk separati che il browser scarica su richiesta.

Pronto a superare i tuoi colloqui su Angular?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Gestire SharedModule e dipendenze comuni

Gli SharedModule, i moduli contenitore che esportano componenti, direttive e pipe di uso comune, sono il blocco più frequente durante la migrazione. Lo schematic non riesce a rimuoverli automaticamente perché molti moduli li importano.

La soluzione: convertire le dichiarazioni condivise in standalone una a una, poi eliminare lo SharedModule quando nessuno lo importa più.

typescript
// Before: SharedModule re-exports everything
@NgModule({
  declarations: [LoadingSpinner, TooltipDirective, TruncatePipe],
  exports: [LoadingSpinner, TooltipDirective, TruncatePipe],
  imports: [CommonModule]
})
export class SharedModule {}

// After: Each declaration is standalone, import directly
// loading-spinner.component.ts
@Component({
  selector: 'app-loading-spinner',
  standalone: true,
  template: \`<div class="spinner" role="status"></div>\`
})
export class LoadingSpinner {}

I consumatori importano direttamente LoadingSpinner invece dell'intero SharedModule. Il bundler include solo i componenti effettivamente necessari a ciascuna rotta.

Imporre lo sviluppo solo standalone

Dopo la migrazione è essenziale impedire che nuovi NgModule rientrino nella codebase. Angular fornisce un'opzione del compilatore TypeScript dedicata.

tsconfig.jsonjson
{
  "angularCompilerOptions": {
    "strictStandalone": true
  }
}

Con strictStandalone attivo, ogni tentativo di creare un componente, direttiva o pipe non standalone produce un errore di compilazione. Questo impone la nuova architettura a tutto il team. La migrazione di Angular Forms segue lo stesso pattern, con Signal Forms ora stabile in Angular 22.

Guadagni di performance: dimensioni del bundle e lazy loading

Il principale beneficio prestazionale degli standalone component arriva dal lazy loading granulare. Con i NgModule il lazy loading agiva a livello di modulo: importare un componente da un modulo trascinava ogni dichiarazione esportata da quel modulo. Gli standalone component spezzano questo accoppiamento.

Un benchmark reale su un'applicazione enterprise di media dimensione (oltre 200 componenti) ha mostrato:

MetricaBasato su NgModuleStandaloneMiglioramento
Bundle iniziale485 KB218 KB-55 %
Chunk lazy più grande142 KB38 KB-73 %
Time to Interactive3,2 s1,8 s-44 %
Tempo di build (esbuild)12,4 s8,1 s-35 %

Questi numeri derivano dalla rimozione dell'overhead di risoluzione dei moduli e dalla capacità del bundler di eliminare gli export inutilizzati a livello di componente invece che a livello di modulo.

Testare gli standalone component con Vitest

Gli unit test si semplificano notevolmente con gli standalone component. Angular 22 ha reso Vitest il test runner predefinito, sostituendo Karma. La configurazione di TestBed non richiede più l'import di interi moduli per soddisfare le dipendenze di un componente.

hero-list.component.spec.tstypescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroListComponent } from './hero-list.component';
import { HeroService } from '../services/hero.service';
import { of } from 'rxjs';

describe('HeroListComponent', () => {
  let fixture: ComponentFixture<HeroListComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [HeroListComponent],
      providers: [
        { provide: HeroService, useValue: { getHeroes: () => of([]) } }
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(HeroListComponent);
  });

  it('should render hero cards', () => {
    fixture.componentRef.setInput('heroes', mockHeroes);
    fixture.detectChanges();
    const cards = fixture.nativeElement.querySelectorAll('app-hero-card');
    expect(cards.length).toBe(mockHeroes.length);
  });
});

Il componente entra direttamente nell'array imports di TestBed.configureTestingModule. Tutte le dipendenze dichiarate sono già risolte tramite il proprio imports, quindi non servono import aggiuntivi di moduli.

Errori comuni durante la migrazione

Import circolari tra standalone component. Quando il componente A importa il B e B importa A, il compilatore TypeScript solleva un errore di dipendenza circolare. La soluzione: estrarre l'interfaccia condivisa in un file separato oppure usare forwardRef() come workaround temporaneo durante il refactoring della catena di dipendenze.

Librerie di terze parti che usano ancora NgModule. Molte librerie sono migrate a standalone, ma alcuni pacchetti legacy esportano ancora NgModule. Questi moduli vanno importati direttamente nell'array imports del componente standalone, Angular supporta la mescolanza di import standalone e basati su moduli.

Provider mancanti dopo la rimozione di AppModule. I service precedentemente forniti nell'array providers del modulo root devono spostarsi nell'ApplicationConfig in app.config.ts o usare providedIn: 'root' nel decoratore @Injectable. I service con scope di rotta dovrebbero usare l'array providers nelle configurazioni di rotta.

Domanda da colloquio

Quando viene chiesto degli standalone component in un colloquio Angular, concentrarsi su tre punti: (1) la gestione delle dipendenze passa dal livello di modulo a quello di componente, (2) il tree-shaking diventa granulare, e (3) il setup dei test si semplifica perché i componenti dichiarano i propri import.

Fonti

Migrazione a standalone: punti chiave per Angular 22

  • Lo schematic ufficiale dell'Angular CLI automatizza l'80-90 % della migrazione in tre passaggi sequenziali: convertire le dichiarazioni, rimuovere i moduli, cambiare il bootstrap
  • I moduli di routing richiedono una conversione manuale da loadChildren con NgModule a loadComponent o file di rotte standalone
  • Gli SharedModule sono l'ostacolo principale: convertire ogni dichiarazione condivisa in standalone individualmente, poi eliminare il modulo
  • Abilitare strictStandalone nel tsconfig per impedire l'introduzione di nuovi NgModule dopo la migrazione
  • Le dimensioni del bundle calano del 30-55 % grazie al tree-shaking a livello di componente e al lazy loading granulare con loadComponent
  • Gli unit test si semplificano con Vitest come runner predefinito: importare il componente standalone direttamente in TestBed senza configurazione di moduli
  • OnPush di default e i selectorless component di Angular 22 si combinano naturalmente con l'architettura standalone per una codebase più pulita e manutenibile

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Sfida del giorno

Sapresti trovare il bug in Angular?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 21 agosto 2026

Tag

#angular
#standalone components
#migration
#angular 22
#ngmodule
#lazy loading

Condividi

Articoli correlati