Startseite / Artikel

Nicht blockierender Angular-Startvorgang: Konfiguration mit rxResource laden

Erfahren Sie, warum ein blockierender APP_INITIALIZER Angular-Nutzer auf einer leeren Seite zurücklässt, und wie rxResource sowie App-Shell- und Route-Resolver ohne ihn Konfigurationen laden.

Verlag: REACTAPP.TOP5 Min. Lesezeit1112 Wörter

Quellen: Stop Blocking Your App Load: Architecting APP_INITIALIZER with Signals

Unternehmensweite Angular-Anwendungen benötigen in der Regel viele Komponenten, bevor die erste nutzbare Ansicht angezeigt werden kann: Feature-Flags, die RBAC-Rechte des Benutzers, das Theme des Mieters sowie einen Telemetrie-Client. Die traditionelle Vorgehensweise besteht darin, einen APP_INITIALIZER zu verwenden, der eine Promise zurückgibt – Angular rendernt bis zur Erfüllung dieser Promise nichts. Im Folgenden sehen Sie, was das kostet, und wie Sie Konfigurationen mit rxResource als reaktiven Zustand behandeln können, sodass die Seite sofort angezeigt wird und nur die Teile warten müssen, die tatsächlich von der Konfiguration abhängen.

Warum ein blockierender Initialisator schädlich ist

Die klassische Einrichtung registriert eine Factory unter dem APP_INITIALIZER-Multi-Provider. Diese Factory gibt eine Funktion zurück, die den Konfigurationsanfrage in eine Promise umwandelt – das Framework startet den Root-Component erst, nachdem diese Promise erfüllt wurde.

// The Blocking Legacy Pipeline
export function initializeConfig(configService: ConfigService) {
  // Angular halts all execution until this Promise resolves
  return () => configService.loadConfiguration().toPromise();
}

// In app.config.ts
providers: [
  {
    provide: APP_INITIALIZER,
    useFactory: initializeConfig,
    deps: [ConfigService],
    multi: true
  }
]

Die Auswirkung ist eine einfache Rechenoperation: Wenn der Konfigurations-Endpunkt 600 ms benötigt, sehen die Benutzer bei jeder Ladeung mindestens 600 ms lang eine leere Seite. Diese Verzögerung fließt direkt in die Zeit bis zum ersten sichtbaren Inhalt ein, und ein langsamer oder fehlerhafter Endpunkt führt zu einer leeren oder nicht funktionierenden Anwendung, ohne dass eine hilfreiche Meldung angezeigt werden kann.

Zwei Details in diesem Auszug sind veraltet. toPromise() ist in RxJS durch firstValueFrom() ersetzt worden, und neuere Angular-Versionen deprecieren das APP_INITIALIZER-Token zugunsten der Funktion provideAppInitializer(). Prüfen Sie die Dokumentation für die von Ihnen genutzte Angular-Version. Keine dieser Änderungen beseitigt jedoch das blockierende Verhalten – sie ändern lediglich die Syntax.

Konfiguration als reaktive Abhängigkeit behandeln

Die Alternative besteht darin, die Konfiguration nicht länger als Zugangspunkt zu behandeln, sondern sie als Daten zu betrachten, die die Benutzeroberfläche erhalten soll. Angels resource()-Familie drückt genau das aus: einen asynchronen Wert, dessen Lade- und Fehlerzustand als Signale bereitgestellt werden.

Ein Konfigurationsdienst, der auf rxResource basiert

Anstatt Angular eine Promise zu übergeben, deklariert der Dienst eine Ressource. rxResource nimmt einen loader entgegen, der ein Observable zurückgibt – in diesem Fall den HttpClient-Aufruf – startet ihn beim Erstellen des Dienstes und stellt value, isLoading sowie error als Signale zur Verfügung. Während des Bootstraps wartet nichts auf diesen Prozess.

// The Non-Blocking Reactive Architecture
@Injectable({ providedIn: 'root' })
export class ConfigService {
  private http = inject(HttpClient);

  // Define a resource that doesn't block the bootstrap
  configResource = rxResource({
    loader: () => this.http.get<AppConfig>('/api/config')
  });

  // Expose as a signal for easy template consumption
  get config() { return this.configResource.value; }
  get isLoading() { return this.configResource.isLoading; }
}

Beachten Sie, dass die Getter selbst die Signale zurückgeben, weshalb die Verbraucher sie aufrufen, wie in configService.isLoading(). Die Ressourcen-APIs haben sich über die verschiedenen Versionen weiterentwickelt (in neueren Versionen wird die Option rxResource als stream statt loader bezeichnet), daher müssen Sie die Namen der Optionen an Ihre Angular-Version anpassen.

Bearbeitung der Konfiguration, ohne die das App nicht laufen kann

Einige Einstellungen sind tatsächlich erforderlich, beispielsweise Werte für die Authentifizierung. Es ist dennoch nicht notwendig, die gesamte App zu blockieren; es gibt zwei bessere Orte, um zu warten.

Option 1: Zuerst eine Schale rendern

Das ist in der Regel die beste Wahl. Die Schale (Header, Navigation, Layout) wird sofort renderiert, und der @if-Control-Flow-Block zeigt ein Skelett, bis die Ressource geladen ist; danach übernimmt der Router-Outlet.

<!-- Non-blocking, instant render -->
@if (configService.isLoading()) {
  <app-skeleton-loader />
} @else {
  <router-outlet />
}

Die Benutzer erhalten sofortige visuelle Rückmeldung, und das Template ist außerdem der geeignete Ort, um Fehlern entgegenzuwirken. Fügen Sie einen Zweig für configResource.error() hinzu, der eine Wiederholungstaste enthält, die reload() des Ressourcenobjekts aufruft; andernfalls endet eine fehlgeschlagene Anfrage mit der Darstellung des Ausgangs ohne Konfiguration.

Option 2: ein Route-Resolver

Falls eine bestimmte Route vor ihrer Aktivierung konfiguriert werden muss, beschränkt ein Resolver das Warten auf diese Navigation statt auf den gesamten Bootstrap-Prozess.

export const configResolver: ResolveFn<AppConfig> = () => {
  return inject(ConfigService).configResource.value!;
};


// In app.routes.ts
{ path: 'dashboard', component: DashboardComponent, resolve: { config: configResolver } }

Achten Sie bei dieser genauen Implementierung darauf. configResource.value ist ein Signal, und die Nicht-Null-Aussage dämpft lediglich den Compiler: Der Resolver gibt das Signal unverzüglich zurück, ohne auf die Anfrage zu warten. Um tatsächlich zu warten, muss der Resolver etwas zurückgeben, das erst dann abgeschlossen ist, wenn die Daten vorhanden sind – beispielsweise ein aus der Ressource abgeleitetes Observable, das den Ladezustand filtert, oder ein direkter HttpClient-Aufruf, der mit firstValueFrom() umhüllt ist.

Vorteile des nicht-blockierenden Ansatzes

  • Schnellere Erstdarstellung. Der Browser kann das HTML-Struktur und die grundlegende CSS-Datei sofort rendern, anstatt ein leeres Wurzelelement anzuzeigen – dadurch verbessern sich die Werte für First Contentful Paint sowie die von Suchmaschinen und Dashboards erfassten Metriken.
  • Bessere Wahrnehmung der Leistung. Skeletons informieren die Benutzer darüber, dass die App aktiv ist, während Flaggen und Berechtigungen im Hintergrund geladen werden.
  • Festigkeit gegenüber Fehlern. Ein fehlerhafter Konfigurationsendpunkt stört den Bootstrap nicht mehr. Die App kann einen Fehlerzustand anzeigen, eine Wiederholung anbieten und gleichzeitig unverwandte Teile des Dashboards weiterhin nutzbar halten.
  • Es gibt einen Kompromiss, den man in Kauf nehmen muss: Jeder Komponente, der Konfigurationen liest, muss nun den Zustand „noch nicht geladen“ handhaben oder hinter einer Grenze (das @if der Shell oder ein Resolver) stehen, die dies gewährleistet. Um diese Zustände sauber zu modellieren, eignet sich eine diskriminierte Union gut, wie in der Ersetzung von Ladeflaggen durch eine AsyncState-Union gezeigt.

    Zusammenfassung

    • Ein Initialisierer, der eine Promise zurückgibt, hält die gesamte Anwendung gefangen – abhängig von der langsamsten Startanfrage.
    • rxResource wandelt Konfigurationen in Signale für Wert, Ladezustand und Fehler um, auf die Templates reagieren können.
    • Standardmäßig wird eine App-Shell mit einem @if-Skelett verwendet; ein Route-Resolver eignet sich für Routen, die ohne Daten nicht aktiviert werden dürfen, vorausgesetzt er wartet tatsächlich.
    • Bewahren Sie Initialisierer für Aufgaben auf, die unbedingt vor dem Renderen abgeschlossen werden müssen – wie beispielsweise die Registrierung eines globalen Fehlerhandlers.

    Egal, ob Sie eine globale Shell oder routebezogene Resolver wählen – das Prinzip bleibt dasselbe: Machen Sie das Warten zu einem sichtbaren, lokalen UI-Zustand anstelle einer leeren Seite.