Angular Standalone Components: Migration und Best Practices für Angular 22

Vollständiger Leitfaden zur Migration von Angular-Anwendungen von NgModules zu Standalone-Komponenten. Behandelt die offizielle 3-stufige CLI-Migration, Selectorless Components, OnPush als Standard und Lazy Loading mit loadComponent in Angular 22.

Architektur der Migration zu Angular Standalone Components: Übergang vom NgModule zum Standalone-Komponentenbaum

Angular Standalone Components machen NgModules überflüssig, reduzieren Boilerplate-Code und ermöglichen feingranulares Lazy Loading in der gesamten Anwendung. Seitdem Angular 19 Standalone zum Standard erklärt hat und Angular 22 Selectorless Components als stabil eingeführt hat, ist die Migration alter modulbasierter Codebasen sowohl unkompliziert als auch wirkungsvoll geworden.

Wichtigste Erkenntnis

Das offizielle Angular-CLI-Schematic übernimmt den Großteil der Migration automatisch in drei Durchgängen. Eine typische Enterprise-Anwendung kann die Konvertierung in einem einzigen Sprint abschließen, wobei die Bundle-Größen dank komponentenbezogenem Lazy Loading um 30-50 % schrumpfen.

NgModules vs. Standalone Components: Was sich geändert hat

NgModules dienten seit Angular 2 als Kompilierungskontext für Komponenten. Jede Komponente, Direktive und Pipe musste in genau einem Modul deklariert werden, und gemeinsam genutzte Funktionalität erforderte sorgfältig orchestrierte Modul-Imports und -Exports. Dies führte zu enger Kopplung zwischen unabhängigen Features und erschwerte das Tree-Shaking.

Standalone Components kehren dieses Modell um. Jede Komponente deklariert ihre eigenen Abhängigkeiten direkt im imports-Array des @Component-Decorators. Keine Modulregistrierung, keine geteilten Module, keine Barrel-Exports der halben Anwendung. Ab Angular 22 wird dieses Muster durch Selectorless Components weiter vereinfacht, die direkte Imports ohne String-Selektoren ermöglichen.

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('');
}

Das imports-Array ersetzt den gesamten NgModule-Abhängigkeitsgraphen. Der Bundler sieht genau, welche Komponenten, Pipes und Direktiven jede Datei benötigt, was präzises Tree-Shaking ermöglicht. Das CommonModule wird in Angular 22 nicht mehr benötigt, da die Control-Flow-Syntax (@for, @if) direkt in den Compiler integriert ist.

Der 3-stufige CLI-Migrationsprozess

Angular bietet ein automatisiertes Schematic, das die Migration in drei aufeinanderfolgenden Durchgängen abwickelt. Jeder Schritt baut auf dem vorherigen auf, und das Projekt sollte zwischen den Durchgängen sauber kompilieren.

Schritt 1: Deklarationen in Standalone umwandeln

Der erste Durchgang scannt jede Komponente, Direktive und Pipe im Projekt, fügt standalone: true hinzu und verschiebt die notwendigen Imports aus dem übergeordneten NgModule in das eigene imports-Array jeder Komponente.

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

Wählen Sie bei der Aufforderung "Convert all components, directives and pipes to standalone" aus. Das Schematic verwendet statische Analyse zur Auflösung von Abhängigkeiten, daher werden Komponenten mit Metadaten, die zur Build-Zeit nicht analysiert werden können, mit einer Warnung übersprungen.

Schritt 2: Überflüssige NgModules entfernen

Da nun alle Deklarationen Standalone sind, werden viele NgModules zu leeren Hüllen. Dieser Durchgang identifiziert Module, die nur Standalone-Deklarationen re-exportiert haben, und entfernt sie.

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

Wählen Sie "Remove unnecessary NgModule classes". Module, die noch Provider, Routenkonfigurationen enthalten oder von mehreren anderen Modulen importiert werden, bleiben mit einem TODO-Kommentar zur manuellen Überprüfung erhalten.

Angular 22 Voraussetzung

Angular 22 erfordert TypeScript 6 und Node.js 26. Ältere Versionen werden nicht unterstützt. Führen Sie ng version aus, um die Kompatibilität vor Beginn der Migration zu prüfen.

Schritt 3: Auf Standalone-Bootstrap umstellen

Der letzte Durchgang ersetzt das Root-NgModule durch Angulars bootstrapApplication-API und konvertiert die Root-Komponente 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]))
  ]
};

Das ApplicationConfig-Muster ersetzt die providers- und imports-Arrays des Root-Moduls. Alle Provider-Funktionen (provideRouter, provideHttpClient, provideAnimations) funktionieren direkt ohne Modul-Wrapper.

Angular 22: OnPush als Standard und Selectorless Components

Angular 22 führte zwei Änderungen ein, die die Entwicklung mit Standalone Components betreffen: OnPush als standardmäßige Change-Detection-Strategie und Selectorless Components als stabil markiert.

OnPush als Standard

Neue Komponenten, die in Angular 22 generiert werden, verwenden standardmäßig ChangeDetectionStrategy.OnPush. Dies fördert signalbasierte Reaktivität gegenüber Zone.js-gesteuerten Prüfungen. Während der Migration fügt das Schematic den bestehenden Komponenten explizit changeDetection: ChangeDetectionStrategy.Default hinzu, um Rückwärtskompatibilität zu gewährleisten.

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');
}

Für migrierte Anwendungen empfiehlt sich die schrittweise Einführung von OnPush durch den Ersatz von Klasseneigenschaften durch Signals. Dies entspricht der Signal-First-Architektur, die Angular 22 fördert.

Selectorless Components (Stabil)

Selectorless Components ermöglichen den direkten Import von Komponenten in Templates ohne Definition von String-Selektoren. Dies eliminiert Namenskonflikte bei Selektoren in großen Codebasen und bietet bessere Typsicherheit.

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>();
}

In Templates werden Selectorless Components über ihren Klassennamen anstelle eines benutzerdefinierten Selektor-Tags verwendet. Der Compiler stellt sicher, dass der Import existiert, und erkennt Fehler zur Build-Zeit statt zur Laufzeit.

Routing-Migration: Von Modulen zu loadComponent

Routing-Module erfordern manuelle Aufmerksamkeit, da das Schematic loadChildren-Modul-Imports nicht automatisch in loadComponent oder Routen-Level-loadChildren mit Standalone-Routen konvertiert.

Das alte Muster lud ganze Feature-Module:

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

Das Standalone-Äquivalent lädt einzelne Komponenten oder Routendateien direkt:

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 lädt eine einzelne Komponente per Lazy Loading. loadChildren mit einer Routendatei lädt einen ganzen Feature-Bereich per Lazy Loading. Beide erzeugen separate Chunks, die der Browser bei Bedarf abruft.

Bereit für deine Angular-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Umgang mit SharedModules und gemeinsamen Abhängigkeiten

SharedModules, die Sammelmodule, die häufig verwendete Komponenten, Direktiven und Pipes exportieren, sind das häufigste Hindernis bei der Migration. Das Schematic kann sie nicht automatisch entfernen, da mehrere Module sie importieren.

Die Lösung: gemeinsame Deklarationen einzeln in Standalone konvertieren und das SharedModule löschen, sobald nichts mehr es importiert.

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 {}

Konsumenten importieren LoadingSpinner jetzt direkt anstelle des gesamten SharedModule. Der Bundler bezieht nur die spezifischen Komponenten ein, die jede Route benötigt.

Standalone-Only-Entwicklung erzwingen

Nach der Migration ist es entscheidend zu verhindern, dass neue NgModules wieder in die Codebasis einsickern. Angular bietet dafür eine TypeScript-Compiler-Option.

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

Mit aktiviertem strictStandalone führt jeder Versuch, eine nicht-standalone Komponente, Direktive oder Pipe zu erstellen, zu einem Kompilierungsfehler. Dies erzwingt die neue Architektur im gesamten Team. Die Angular-Forms-Migration folgt dem gleichen Muster, wobei Signal Forms in Angular 22 nun stabil sind.

Performance-Gewinne: Bundle-Größe und Lazy Loading

Der primäre Performance-Vorteil von Standalone Components ergibt sich aus granularem Lazy Loading. Mit NgModules funktionierte Lazy Loading auf Modulebene: der Import einer Komponente aus einem Modul zog jede Deklaration mit, die das Modul exportierte. Standalone Components durchbrechen diese Kopplung.

Ein Real-World-Benchmark mit einer mittelgroßen Enterprise-Anwendung (200+ Komponenten) zeigte:

MetrikNgModule-basiertStandaloneVerbesserung
Initial Bundle485 KB218 KB-55 %
Größter Lazy-Chunk142 KB38 KB-73 %
Time to Interactive3,2 s1,8 s-44 %
Build-Zeit (esbuild)12,4 s8,1 s-35 %

Diese Zahlen ergeben sich aus dem Wegfall des Overheads der Modulauflösung und der Möglichkeit für den Bundler, ungenutzte Exporte auf Komponentenebene statt auf Modulebene zu eliminieren.

Standalone Components mit Vitest testen

Unit-Tests werden mit Standalone Components deutlich einfacher. Angular 22 hat Vitest zum Standard-Testrunner gemacht und ersetzt damit Karma. Die TestBed-Konfiguration erfordert nicht länger den Import ganzer Module, um die Abhängigkeiten einer Komponente zu erfüllen.

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);
  });
});

Die Komponente landet direkt im imports-Array von TestBed.configureTestingModule. Alle deklarierten Abhängigkeiten sind bereits über das eigene imports aufgelöst, sodass keine zusätzlichen Modul-Imports nötig sind.

Häufige Migrationsfallen

Zirkuläre Imports zwischen Standalone Components. Wenn Komponente A Komponente B importiert und B wiederum A, wirft der TypeScript-Compiler einen Fehler wegen zirkulärer Abhängigkeit. Die Lösung: das gemeinsame Interface in eine separate Datei extrahieren oder forwardRef() als temporären Workaround verwenden, während die Abhängigkeitskette refaktoriert wird.

Drittanbieter-Bibliotheken nutzen noch NgModules. Viele Bibliotheken sind zu Standalone migriert, einige Legacy-Pakete exportieren jedoch weiterhin NgModules. Diese Module können direkt im imports-Array der Standalone-Komponente importiert werden, Angular unterstützt das Mischen von Standalone- und modulbasierten Imports.

Fehlende Provider nach Entfernung des AppModule. Services, die zuvor im providers-Array des Root-Moduls bereitgestellt wurden, müssen in das ApplicationConfig in app.config.ts verschoben werden oder providedIn: 'root' im @Injectable-Decorator verwenden. Routen-bezogene Services sollten das providers-Array in den Routenkonfigurationen nutzen.

Interview-Frage

Bei der Frage nach Standalone Components in einem Angular-Interview sollten drei Punkte im Fokus stehen: (1) Abhängigkeitsmanagement verlagert sich von der Modul- auf die Komponentenebene, (2) Tree-Shaking wird granular, und (3) der Test-Setup vereinfacht sich, da Komponenten ihre eigenen Imports deklarieren.

Quellen

Migration zu Standalone: Die wichtigsten Punkte für Angular 22

  • Das offizielle Angular-CLI-Schematic automatisiert 80-90 % der Migration durch drei aufeinanderfolgende Durchgänge: Deklarationen konvertieren, Module entfernen, Bootstrap umstellen
  • Routing-Module benötigen eine manuelle Konvertierung von loadChildren mit NgModules zu loadComponent oder Standalone-Routendateien
  • SharedModules sind das Hauptproblem: jede gemeinsame Deklaration einzeln in Standalone konvertieren und dann das Modul löschen
  • strictStandalone in der tsconfig aktivieren, um zu verhindern, dass nach der Migration neue NgModules eingeführt werden
  • Bundle-Größen sinken um 30-55 % dank komponentenbezogenem Tree-Shaking und granularem Lazy Loading mit loadComponent
  • Unit-Tests werden mit Vitest als Standard-Runner einfacher: die Standalone-Komponente direkt in TestBed ohne Modulkonfiguration importieren
  • Angular 22s OnPush-Standard und Selectorless Components passen natürlich zur Standalone-Architektur für eine sauberere, wartbarere Codebasis

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tägliche Challenge

Findest du den Bug in Angular?

Ein echter Codeausschnitt, ein versteckter Bug, ein Versuch pro Tag. Zum Ausprobieren ohne Konto.

Anthony Fillion-Maillet

Geschrieben von

Anthony Fillion-Maillet

Gründer von SharpSkill

Seit über 10 Jahren Fullstack-Entwickler. Er leitet SharpSkill und verantwortet alles, was hier erscheint.

Aktualisiert am 21. August 2026

Tags

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

Teilen

Verwandte Artikel