Jede Angular-Anwendung beginnt ihren Lebenszyklus an einem einzigen Punkt: der Datei main.ts. Sie ist der Einstieg, den der Build-Prozess (esbuild) als Wurzel des gesamten Modul-Graphen verwendet. Von hier aus wird Angular initialisiert, der Dependency-Injection-Tree aufgebaut, die Root-Komponente in den DOM-Knoten gerendert und die Change-Detection gestartet.
Seit Angular 14 (Default ab v19) erfolgt dieser Schritt mit bootstrapApplication() und einer ApplicationConfig. Der klassische, NgModule-basierte Weg über platformBrowserDynamic().bootstrapModule() ist weiterhin First-Class und gerade in größeren Bestands-Codebasen relevant — beide Welten sind gleichwertig.
main.ts als Einstiegspunkt
Die main.ts ist die einzige Datei, die der Angular-CLI-Build als Entry-Point kennt. Im angular.json ist sie unter projects.<name>.architect.build.options.main (bzw. browser) verdrahtet. Beim ng build analysiert esbuild diese Datei, folgt allen import-Anweisungen rekursiv und erzeugt aus dem entstehenden Graph einen optimierten, tree-shakeable JavaScript-Bundle.
Konzeptuell passieren beim Start drei Dinge:
- Der Browser lädt das gebündelte JavaScript und führt
main.tsaus. - Angular initialisiert die
PlatformRef(genau eine pro Seite, verwaltet plattform-globale Services). - Auf der Plattform wird die Anwendung gestartet — entweder als Standalone-App über
bootstrapApplication()oder als NgModule-App überplatformBrowserDynamic().bootstrapModule(). Das Ergebnis ist eineApplicationRef, die den Lebenszyklus der laufenden App repräsentiert.
In index.html existiert dafür ein einziger Host-Tag (z. B. <app-root></app-root>), in den Angular die Root-Komponente rendert.
Standalone-Bootstrap mit bootstrapApplication
Der moderne Weg trennt Wurzel-Komponente und Konfiguration sauber: main.ts ruft bootstrapApplication(AppComponent, appConfig) auf, die appConfig lebt in einer eigenen Datei app.config.ts. Diese Trennung erleichtert SSR (eine zweite, server-spezifische Konfig daneben) und macht die Provider-Liste dokumentar-zentral.
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig)
.then(appRef => {
// optionaler Hook nach erfolgreichem Start, z. B. Diagnostics
console.log('App ready', appRef.components.length);
})
.catch(err => console.error(err));Die ApplicationConfig ist ein simples Objekt mit einem einzigen Feld providers. Anders als bei NgModules werden hier keine Komponenten oder Module deklariert — nur Provider-Funktionen, die Angular-Subsysteme aktivieren.
import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(),
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(withFetch()),
provideAnimationsAsync(),
provideClientHydration(withEventReplay()),
],
};Die Signatur lautet bootstrapApplication(rootComponent, options?): Promise<ApplicationRef>. Die Wurzel-Komponente muss standalone sein (was seit v19 sowieso der Default ist). Provider werden ausschließlich über Funktionen wie provideRouter() registriert — diese Provider-Funktionen sind tree-shakeable, ungenutzte Subsysteme landen also gar nicht erst im Bundle.
Klassischer NgModule-Bootstrap
Vor der Standalone-Ära gruppierte Angular Komponenten, Direktiven und Pipes in NgModule-Klassen. Der Bootstrap erfolgt dann zweistufig: Zuerst wird über platformBrowserDynamic() eine PlatformRef erzeugt, anschließend wird das AppModule darauf gestartet. Dieser Weg ist nicht „legacy” — Angular pflegt ihn aktiv, und in großen Bestands-Codebasen ist er weiterhin die natürliche Wahl.
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic()
.bootstrapModule(AppModule)
.then(moduleRef => {
// moduleRef.injector liefert Zugriff auf alle Provider des Root-Injectors
console.log('Module bootstrapped');
})
.catch(err => console.error(err));Das AppModule selbst bündelt drei Aspekte: declarations listet alle Komponenten, Direktiven und Pipes, die zu diesem Modul gehören; imports zieht Feature-Module mit weiteren Providern hinein; bootstrap benennt die Wurzel-Komponente, die in index.html gerendert wird.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { routes } from './app.routes';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
RouterModule.forRoot(routes),
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}Der konzeptuelle Unterschied: NgModules deklarieren Bausteine (was zur App gehört), Standalone-Provider aktivieren Subsysteme (was die App können soll). Beide produzieren am Ende die gleiche ApplicationRef.
Provider/Module-Mapping
Wer aus einer NgModule-Welt kommt, sucht oft das Pendant zu einem bekannten Modul. Die Übersicht zeigt, dass beide Wege denselben Funktionsumfang abdecken — nur eben unter verschiedenen API-Formen.
| Standalone-Provider | NgModule-Import | Zweck |
|---|---|---|
provideRouter(routes) | RouterModule.forRoot(routes) | Routing-Konfiguration |
provideHttpClient() | HttpClientModule | HTTP-Zugriff |
provideAnimationsAsync() | BrowserAnimationsModule | Animations-Engine |
provideClientHydration() | (Teil von BrowserModule.withServerTransition) | SSR-Hydration |
provideZonelessChangeDetection() | (kein direktes Modul-Pendant) | Zoneless Change-Detection |
importProvidersFrom(SomeModule) | imports: [SomeModule] | Brücke aus NgModule-Welt |
Die Wahl ist pragmatisch: Neue Greenfield-Projekte fahren standalone — kleinere Bundles, klarere DI, bessere Tree-Shaking-Eigenschaften. Bestehende Apps mit hunderten NgModules können schrittweise migrieren oder bewusst beim NgModule-Stil bleiben. importProvidersFrom() ist die offizielle Brücke, die NgModule-Provider in eine ApplicationConfig einschleust.
Zoneless und experimentelle Provider
Mit Angular 18+ stehen Provider zur Verfügung, die das Laufzeitverhalten grundlegend verändern. Sie gehören nicht zwingend in jede App, sind aber wichtige Stellschrauben am Bootstrap.
import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
import { provideClientHydration, withEventReplay, withIncrementalHydration } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
// Zoneless: Change-Detection wird durch Signals ausgelöst, zone.js wird nicht mehr benötigt
provideZonelessChangeDetection(),
provideRouter(routes),
// SSR-Hydration: rehydriert serverseitig gerendertes HTML
provideClientHydration(
withEventReplay(), // wiederholt Klicks, die vor Hydration passieren
withIncrementalHydration() // hydratisiert nur sichtbare Bereiche (v19+)
),
],
};Beim NgModule-Bootstrap landen diese Provider im providers-Array des AppModule — der Mechanismus ist identisch, nur die Verpackung unterscheidet sich.
Besonderheiten
bootstrapApplication ist tree-shakeable
Provider-Funktionen wie provideRouter() oder provideHttpClient() werden vom Bundler nur eingebunden, wenn sie tatsächlich aufgerufen werden. Der klassische NgModule-Weg zieht ganze Module-Bäume mit allen Direktiven und Pipes in den Graph, auch wenn nur ein Bruchteil davon genutzt wird. Standalone-Apps starten daher mit deutlich kleinerem initialen Bundle.
Mehrere bootstrapApplication-Aufrufe für Mikro-Frontends
Eine HTML-Seite kann mehrere unabhängige Standalone-Apps gleichzeitig starten — jede mit eigenem Root-Komponenten-Tag, eigener ApplicationConfig und eigenem Injector. Mit dem klassischen NgModule-Bootstrap geht das nicht ohne Weiteres, weil platformBrowserDynamic() die Plattform-Singleton-Logik kapselt.
ApplicationConfig wiederverwenden
Da ApplicationConfig nur ein Objekt ist, lassen sich Basis-Provider in einer geteilten Konstante kapseln und per Spread (…baseConfig.providers) in App-spezifische Configs einsetzen. Praktisch für Monorepos mit mehreren Apps, die einen gemeinsamen Provider-Stack teilen.
Promise-Hook nach Initialisierung
Sowohl bootstrapApplication().then(appRef => …) als auch bootstrapModule().then(moduleRef => …) liefern eine Referenz auf die fertig initialisierte App. Das ist der saubere Hook-Punkt für Setup-Code, der erst nach dem ersten Render sinnvoll ist — etwa Telemetrie-Init oder das Registrieren von Service-Workern.
APP_INITIALIZER für asynchrones Setup vor Render
Wenn die App vor dem ersten Render zwingend etwas laden muss (Config-File, i18n-Daten, Feature-Flags), gibt es den APP_INITIALIZER-Token. Er nimmt eine Factory entgegen, die ein Promise zurückgibt — Angular wartet auf dessen Auflösung, bevor die Wurzel-Komponente gerendert wird. Funktioniert in beiden Welten (Standalone-Provider-Array und NgModule-providers).
Weiterführende Ressourcen
Externe Quellen
- bootstrapApplication API – Angular.dev
- ApplicationConfig API – Angular.dev
- PlatformRef API – Angular.dev
- NgModule-Bootstrapping – Angular.dev