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:

  1. Der Browser lädt das gebündelte JavaScript und führt main.ts aus.
  2. Angular initialisiert die PlatformRef (genau eine pro Seite, verwaltet plattform-globale Services).
  3. Auf der Plattform wird die Anwendung gestartet — entweder als Standalone-App über bootstrapApplication() oder als NgModule-App über platformBrowserDynamic().bootstrapModule(). Das Ergebnis ist eine ApplicationRef, 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.

TypeScript main.ts
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.

TypeScript app.config.ts
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.

TypeScript main.ts
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.

TypeScript app.module.ts
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-ProviderNgModule-ImportZweck
provideRouter(routes)RouterModule.forRoot(routes)Routing-Konfiguration
provideHttpClient()HttpClientModuleHTTP-Zugriff
provideAnimationsAsync()BrowserAnimationsModuleAnimations-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.

TypeScript app.config.ts
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

/ Weiter

Zurück zu Grundlagen

Zur Übersicht