Angular Standalone Components: migrazione e best practice nel 2026

Guida completa alla migrazione di applicazioni Angular dai NgModule agli standalone component. Copre la migrazione ufficiale CLI in 3 passaggi, lazy loading, routing e best practice per Angular 21.

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 21 ha consolidato la change detection zoneless, 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.

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

@Component({
  selector: 'app-hero-list',
  standalone: true,
  imports: [CommonModule, 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 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 g @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 g @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.

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 } 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: [
    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.

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.

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:

| Metrica | Basato su NgModule | Standalone | Miglioramento | |--------|---------------|------------|-------------| | Bundle iniziale | 485 KB | 218 KB | -55 % | | Chunk lazy più grande | 142 KB | 38 KB | -73 % | | Time to Interactive | 3,2 s | 1,8 s | -44 % | | Tempo di build (esbuild) | 12,4 s | 8,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

Gli unit test si semplificano notevolmente con gli standalone component. 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';

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.

Inizia a praticare!

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

Conclusione

  • 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 drasticamente: importare il componente standalone direttamente in TestBed senza configurazione di moduli
  • La change detection zoneless di Angular 21 e la reattività basata sui signal si combinano naturalmente con l'architettura standalone per la massima performance

Inizia a praticare!

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

Tag

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

Condividi

Articoli correlati