Démarrage d’Angular non bloquant : Chargement de la configuration avec rxResource
Découvrez pourquoi un APP_INITIALIZER bloquant laisse les utilisateurs d’Angular sur une page blanche, et comment rxResource, ainsi que des résolveurs de shell d’application et de routes, chargent la configuration en l’absence de ce mécanisme.
Sources: Stop Blocking Your App Load: Architecting APP_INITIALIZER with Signals
Les applications Angular d’entreprise nécessitent généralement de nombreux éléments avant d’afficher la première interface utile : des flags fonctionnels, les permissions RBAC de l’utilisateur, le thème du locataire, ainsi qu’un client de télémétrie. La méthode traditionnelle consiste à utiliser un APP_INITIALIZER qui retourne une promesse, et Angular ne rend rien tant que cette promesse n’est pas résolue. Ci-dessous, vous verrez quel est le coût de cette approche, ainsi que la manière de traiter la configuration comme un état réactif à l’aide de rxResource, afin que la page s’affiche immédiatement et que seules les parties véritablement dépendantes de la configuration attendent.
Pourquoi un initialiseur bloquant est problématique
La configuration classique enregistre une usine sous le multi-provideur APP_INITIALIZER. Cette usine retourne une fonction qui convertit la demande de configuration en une promesse, et le framework ne démarrera pas le composant racine tant que cette promesse n’est pas résolue.
// 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
}
]
L’effet est une simple opération arithmétique : si l’endpoint de configuration met 600 ms à répondre, les utilisateurs voient une page vide pendant au moins 600 ms à chaque chargement. Ce délai est directement pris en compte dans le temps de First Contentful Paint, et un endpoint lent ou défaillant se transforme en une application vide ou dysfonctionnelle, sans possibilité d’afficher un message utile.
Deux éléments dans cet extrait sont désuets. La fonction toPromise() est dépréciée dans RxJS au profit de firstValueFrom(), et les versions récentes d’Angular déprécient le token APP_INITIALIZER au profit de la fonction provideAppInitializer(). Vérifiez la documentation correspondant à la version d’Angular que vous utilisez. Cependant, aucune de ces modifications ne supprime le comportement bloquant ; elles se contentent de changer la syntaxe.
Considérer la configuration comme une dépendance réactive
L’alternative consiste à cesser de considérer la configuration comme une étape obligatoire et à commencer à la traiter comme des données que l’interface utilisateur s’attend à recevoir. La famille resource() d’Angular exprime précisément cela : une valeur asynchrone dont l’état de chargement et d’erreur est exposé sous forme de signaux.
Un service de configuration basé sur rxResource
Au lieu de fournir à Angular une promesse, le service déclare une ressource. rxResource prend un chargeur qui retourne un Observable, ici l’appel à HttpClient, le lance lors de la création du service, et expose value, isLoading et error en tant que signaux. Rien n’attend son exécution pendant le démarrage du projet.
// 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; }
}
Notez que les getters retournent eux-mêmes les signaux, ce qui fait que les consommateurs les appellent, comme dans configService.isLoading(). Les API de ressources évoluent à chaque nouvelle version (dans les versions plus récentes, l’option rxResource s’appelle stream plutôt que loader), il convient donc d’adapter les noms des options à votre version d’Angular.
Gestion de la configuration sans laquelle l’application ne peut pas fonctionner
Certaines paramétrisations sont véritablement nécessaires, par exemple les valeurs requises pour l’authentification. Il n’est pas encore nécessaire de bloquer toute l’application ; il existe deux endroits plus appropriés pour attendre.
Option 1 : afficher d’abord un shell
C’est généralement le meilleur choix. Le shell (en-tête, navigation, mise en page) s’affiche immédiatement, et le bloc de flux de contrôle @if affiche un squelette jusqu’à ce que la ressource ait fini de se charger, après quoi l’élément router outlet prend le relais.
<!-- Non-blocking, instant render -->
@if (configService.isLoading()) {
<app-skeleton-loader />
} @else {
<router-outlet />
}
Les utilisateurs reçoivent une réponse visuelle immédiate, et le modèle constitue également un endroit idéal pour gérer les échecs. Ajoutez une branche pour configResource.error() avec un bouton de tentative qui appelle la méthode reload() du resource ; sinon, une demande échouée se termine par l’affichage d’une page sans configuration.
Option 2 : un résolveur de route
Si une route spécifique nécessite une configuration avant d’être activée, un résolveur limite l’attente à cette navigation plutôt qu’à tout le processus de démarrage.
export const configResolver: ResolveFn<AppConfig> = () => {
return inject(ConfigService).configResource.value!;
};
// In app.routes.ts
{ path: 'dashboard', component: DashboardComponent, resolve: { config: configResolver } }
Faites attention à cette implémentation précise. configResource.value est un signal, et l’assertion non-null ne fait que faire taire le compilateur : le résolveur renvoie immédiatement ce signal sans attendre la demande. Pour vraiment attendre, le résolveur doit retourner quelque chose qui se termine une fois les données disponibles, par exemple un Observable dérivé de la ressource qui filtre l’état de chargement, ou une appel direct à HttpClient enveloppé dans firstValueFrom().
Avantages de l’approche non bloquante
- Déploiement plus rapide. Le navigateur peut afficher immédiatement l’HTML de base et le CSS de base au lieu d’un élément racine vide, ce qui améliore le First Contentful Paint ainsi que les métriques suivies par les moteurs de recherche et les tableaux de bord.
Il faut accepter un compromis : chaque composant qui lit la configuration doit désormais gérer l’état « pas encore chargé », ou être placé derrière une barrière (le @if du shell ou un résolveur) qui en garantit le traitement. Pour modéliser ces états de manière claire, une union discriminée s’avère efficace, comme illustré dans la substitution des drapeaux de chargement par une union AsyncState.
Conclusion
- Un initialiseur qui retourne une promesse retient toute l’application en otage en fonction de sa requête de démarrage la plus lente.
rxResourcetransforme les paramètres de configuration en signaux pour la valeur, le chargement et les erreurs, auxquels les templates peuvent réagir.- Un noyau d’application avec un squelette
@ifest la configuration par défaut ; un résolveur de route s’adapte aux routes qui ne doivent pas s’activer sans données, à condition qu’il attende réellement. - Réservez les initialiseurs aux tâches qui doivent vraiment être terminées avant que quoi que ce soit ne s’affiche, comme l’enregistrement d’un gestionnaire d’erreurs global.
Que vous choisissiez un noyau global ou des résolveurs par route, le principe reste le même : faire en sorte que l’attente se manifeste par un état UI local visible, plutôt qu’une page vide.