Appearance
Hydratation incrémentielle dans Angular
L'hydratation incrémentielle est une technique avancée qui permet d'optimiser les performances de votre application Angular avec SSR en hydratant progressivement les différentes parties de votre interface plutôt que tout d'un coup. Dans ce tutoriel, nous allons découvrir comment cette fonctionnalité révolutionne l'expérience utilisateur et améliore les métriques de performance.
Il est recommandé de lire l'article Hydratation avant de lire ce tutoriel.
Comprendre l'hydratation incrémentielle avec un exemple concret
Imaginez que vous visitez un musée. Au lieu d'allumer toutes les lumières de toutes les salles en même temps (ce qui serait coûteux et inutile), le musée allume progressivement les lumières des salles que vous êtes sur le point de visiter. Cela permet d'économiser de l'énergie et de rendre l'expérience plus fluide.

L'hydratation incrémentielle fonctionne de la même manière :
- Le serveur génère tout le HTML : Tous les contenus sont déjà présents dans le HTML initial
- Le navigateur affiche immédiatement : L'utilisateur voit le contenu sans attendre
- Angular hydrate progressivement : Seules les parties nécessaires sont rendues interactives au bon moment
CONCEPT CLÉ
L'hydratation incrémentielle permet de réduire la charge JavaScript initiale en ne rendant interactives que les parties de l'application qui en ont réellement besoin, au moment où elles en ont besoin.
Pourquoi utiliser l'hydratation incrémentielle ?
Lorsque vous utilisez le SSR avec l'hydratation classique, Angular hydrate toute l'application d'un coup au démarrage. Cela peut être problématique pour plusieurs raisons :
- Charge JavaScript importante : Tous les composants sont hydratés simultanément
- Temps d'interactivité (TTI) élevé : L'utilisateur doit attendre que tout soit prêt
- Impact sur le main thread : Le navigateur peut être bloqué pendant l'hydratation
- Métriques Core Web Vitals dégradées : INP, CLS peuvent être affectés
L'hydratation incrémentielle résout ces problèmes en permettant de :
- Réduire le JavaScript initial : Seuls les composants critiques sont hydratés au démarrage
- Améliorer le TTI : L'application devient interactive plus rapidement
- Optimiser les ressources : Les composants non visibles ou peu utilisés peuvent ne jamais être hydratés
- Améliorer les métriques : FCP, LCP, INP et CLS sont généralement améliorés
Prérequis
Avant de commencer, assurez-vous que votre application :
- Utilise déjà le SSR (Server-Side Rendering)
- A l'hydratation client de base activée avec
provideClientHydration()
Si ce n'est pas le cas, vous pouvez ajouter le SSR à votre projet avec :
bash
ng add @angular/ssrActiver l'hydratation incrémentielle
Pour activer l'hydratation incrémentielle dans votre application, vous devez modifier votre configuration pour inclure withIncrementalHydration() :
ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
provideClientHydration(withIncrementalHydration())
]
};EVENT REPLAY AUTOMATIQUE
Lorsque vous activez l'hydratation incrémentielle avec withIncrementalHydration(), le système d'event replay est automatiquement intégré. Cela signifie que les interactions utilisateur (clics, touches, etc.) qui se produisent avant l'hydratation d'un composant seront capturées et rejouées une fois que le composant sera hydraté.
Les triggers d'hydratation
Une fois l'hydratation incrémentielle activée, vous pouvez contrôler quand chaque partie de votre application doit être hydratée en utilisant des triggers d'hydratation avec la directive @defer.
TRIGGERS RÉGULIERS VS TRIGGERS D'HYDRATATION
Les triggers d'hydratation sont des triggers additionnels qui peuvent être utilisés en plus des triggers réguliers @defer (comme on viewport, on idle, etc.).
- Triggers réguliers : Contrôlent quand le contenu est chargé et rendu (applicable à tous les rendus, y compris client-side)
- Triggers d'hydratation : Contrôlent uniquement quand le contenu est hydraté lors du chargement initial SSR
Vous pouvez combiner les deux : @defer (on viewport; hydrate on interaction) signifie que le contenu sera chargé au viewport, mais hydraté seulement lors d'une interaction (au premier chargement SSR).
hydrate on idle
Hydrate le composant lorsque le navigateur est inactif. Mais que signifie exactement "inactif" ?
Le navigateur est considéré comme inactif lorsqu'il n'a pas de tâches prioritaires à exécuter. Angular utilise l'API requestIdleCallback du navigateur pour détecter ces moments d'inactivité. Concrètement, cela signifie que :
- Le navigateur a terminé de traiter les événements utilisateur en attente
- Les animations et transitions sont en pause
- Le navigateur n'a pas de tâches critiques à exécuter
- Le thread principal est disponible pour des tâches non urgentes
C'est idéal pour les contenus secondaires qui n'ont pas besoin d'être interactifs immédiatement, comme les footers, les liens de navigation secondaires, ou les widgets non essentiels.
angular-html
@defer (hydrate on idle) {
<app-user-footer />
} @placeholder {
<footer>Footer</footer>
}QUAND UTILISER "hydrate on idle"
Utilisez hydrate on idle pour les contenus qui :
- Ne sont pas visibles immédiatement (en bas de page, dans des sections secondaires)
- N'ont pas besoin d'interactivité immédiate
- Peuvent attendre que le navigateur ait terminé toutes ses tâches prioritaires
hydrate on viewport
Hydrate le composant lorsqu'il entre dans le viewport (visible à l'écran). Utilise l'Intersection Observer API pour détecter la visibilité.
angular-html
@defer (hydrate on viewport) {
<app-user-statistics />
} @placeholder {
<div>Statistiques utilisateur</div>
}hydrate on interaction
Hydrate le composant lorsque l'utilisateur interagit avec l'élément spécifié via des événements de clic ou de touche (keydown). Le composant reste visible mais n'est pas interactif jusqu'à l'interaction.
angular-html
@defer (on viewport; hydrate on interaction) {
<app-user-dashboard />
} @placeholder {
<div>Tableau de bord</div>
}Dans cet exemple, le contenu sera chargé quand il entre dans le viewport, mais l'hydratation ne se fera que lors d'une interaction utilisateur (au premier chargement SSR).
hydrate on hover
Hydrate le composant lorsque l'utilisateur survole la zone concernée via les événements mouseover et focusin. Utile pour les composants qui nécessitent une interaction au survol.
angular-html
@defer (hydrate on hover) {
<app-user-tooltip />
} @placeholder {
<div>Survolez pour voir les détails</div>
}hydrate on immediate
Hydrate le composant immédiatement après que tout le contenu non-déféré ait fini de se rendre. Cela signifie que le bloc déféré se charge dès que possible, mais seulement après le rendu du contenu prioritaire.
angular-html
@defer (hydrate on immediate) {
<app-priority-widget />
} @placeholder {
<div>Widget prioritaire</div>
}hydrate on timer
Hydrate le composant après une durée spécifiée. La durée peut être exprimée en millisecondes (ms) ou en secondes (s).
angular-html
@defer (on viewport; hydrate on timer(2s)) {
<app-user-recommendations />
} @placeholder {
<div>Recommandations</div>
}Ou en millisecondes :
angular-html
@defer (on viewport; hydrate on timer(2000ms)) {
<app-user-recommendations />
} @placeholder {
<div>Recommandations</div>
}hydrate when condition
Hydrate le composant lorsqu'une condition (signal, fonction ou booléen) devient vraie. Offre un contrôle total sur le moment de l'hydratation.
ts
@Component({
template: `
@defer (on viewport; hydrate when isUserLoggedIn()) {
<app-user-profile />
} @placeholder {
<div>Profil utilisateur</div>
}
`
})
export class DashboardComponent {
// Signal qui détermine si l'utilisateur est connecté
private authService = inject(AuthService);
isUserLoggedIn = computed(() => this.authService.isAuthenticated());
}IMPORTANT POUR "hydrate when"
Les conditions hydrate when ne se déclenchent que lorsque le bloc @defer est le bloc déshydraté le plus haut dans la hiérarchie. La condition doit être spécifiée dans le composant parent, qui doit exister avant de pouvoir être déclenchée. Si le bloc parent est déshydraté, l'expression ne sera pas encore résolvable par Angular.
hydrate never
Ne jamais hydrater le composant lors du chargement initial SSR. Utile pour du contenu purement statique qui n'a pas besoin d'être interactif.
angular-html
@defer (on viewport; hydrate never) {
<app-static-content />
} @placeholder {
<div>Contenu statique</div>
}COMPORTEMENT DE "hydrate never"
hydrate never s'applique uniquement au rendu initial SSR. Lors d'un rendu client-side ultérieur (par exemple après une navigation), le bloc @defer chargera normalement ses dépendances selon le trigger régulier (dans l'exemple ci-dessus, on viewport). De plus, hydrate never empêche l'hydratation de toute la sous-arborescence imbriquée - aucun autre trigger d'hydratation ne se déclenchera pour le contenu imbriqué sous ce bloc.
Exemple pratique : Application de gestion d'utilisateurs
Créons un exemple concret avec une application de gestion d'utilisateurs pour illustrer l'utilisation de l'hydratation incrémentielle.
Structure de l'application
Nous allons créer une page de tableau de bord avec plusieurs sections ayant différents besoins d'hydratation :
- Header : Hydraté immédiatement (critique)
- Liste d'utilisateurs : Hydratée au viewport
- Statistiques : Hydratée après interaction
- Footer : Hydratée en idle
- Recommandations : Hydratée après un délai
Voici comment structurer notre composant :
ts
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserService } from '../core/services/user.service';
import { User } from '../core/interfaces/user.interface';
import { UserListComponent } from '../components/features/user/user-list.component';
import { UserStatisticsComponent } from '../components/features/user/user-statistics.component';
import { UserRecommendationsComponent } from '../components/features/user/user-recommendations.component';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [
CommonModule,
UserListComponent,
UserStatisticsComponent,
UserRecommendationsComponent
],
template: `
<!-- Header hydraté immédiatement (pas de @defer) -->
<header>
<h1>Tableau de bord utilisateurs</h1>
</header>
<main>
<!-- Liste d'utilisateurs : hydratée au viewport -->
<section>
@defer (hydrate on viewport) {
<app-user-list [users]="users()" />
} @placeholder {
<div class="placeholder">Chargement de la liste des utilisateurs...</div>
}
</section>
<!-- Statistiques : chargée au viewport, hydratée après interaction -->
<section>
@defer (on viewport; hydrate on interaction) {
<app-user-statistics [users]="users()" />
} @placeholder {
<div class="placeholder">Cliquez pour voir les statistiques</div>
}
</section>
<!-- Recommandations : chargée au viewport, hydratée après 2 secondes -->
<section>
@defer (on viewport; hydrate on timer(2s)) {
<app-user-recommendations [users]="users()" />
} @placeholder {
<div class="placeholder">Recommandations</div>
}
</section>
</main>
<!-- Footer : hydratée en idle -->
<footer>
@defer (hydrate on idle) {
<app-user-footer />
} @placeholder {
<div>Footer</div>
}
</footer>
`
})
export class DashboardComponent {
// Injection du service utilisateur
private userService = inject(UserService);
// Signal contenant la liste des utilisateurs
users = this.userService.getUsers();
}Explication de la stratégie
Dans cet exemple, nous avons appliqué différentes stratégies d'hydratation selon l'importance et l'usage de chaque section :
- Header : Pas de
@defercar il doit être immédiatement interactif (navigation, etc.) - Liste d'utilisateurs : Hydratée au viewport car elle est importante mais peut attendre d'être visible
- Statistiques : Chargée au viewport mais hydratée à l'interaction - le contenu est visible mais n'est pas interactif jusqu'à l'interaction utilisateur
- Recommandations : Chargée au viewport mais hydratée après un délai pour laisser la priorité aux contenus plus importants
- Footer : Hydratée en idle car c'est le contenu le moins prioritaire
Comprendre la différence entre triggers réguliers et triggers d'hydratation
Il est important de comprendre la distinction entre les triggers réguliers et les triggers d'hydratation :
Triggers réguliers (on viewport, on idle, etc.) :
- Contrôlent quand le contenu est chargé et rendu
- S'appliquent à tous les rendus (SSR initial et rendus client-side ultérieurs)
- Déterminent quand les dépendances sont chargées et quand le contenu apparaît
Triggers d'hydratation (hydrate on ...) :
- Contrôlent uniquement quand le contenu est hydraté (rendu interactif)
- S'appliquent uniquement au chargement initial SSR
- Lors des rendus client-side ultérieurs, seuls les triggers réguliers s'appliquent
Exemple concret :
angular-html
@defer (on idle; hydrate on interaction) {
<example-cmp />
} @placeholder {
<div>Example Placeholder</div>
}Dans cet exemple :
- Au chargement initial SSR : Le trigger
hydrate on interactions'applique. Le contenu sera hydraté lors d'une interaction utilisateur. - Lors d'un rendu client-side ultérieur (par exemple après une navigation) : Seul le trigger
on idles'applique. Le contenu sera chargé quand le navigateur est inactif.
Blocs @defer imbriqués
Lorsque vous avez des blocs @defer imbriqués (un @defer à l'intérieur d'un autre), il est important de comprendre l'ordre d'hydratation :
Règle importante : Le système de composants et de dépendances d'Angular est hiérarchique. Pour hydrater un composant enfant, tous ses parents doivent d'abord être hydratés. Si l'hydratation est déclenchée pour un bloc @defer enfant dans un ensemble de blocs déshydratés imbriqués, l'hydratation se déclenche depuis le bloc @defer déshydraté le plus haut jusqu'au bloc enfant déclenché, dans cet ordre.
ts
@Component({
template: `
@defer (hydrate on interaction) {
<app-parent-component>
@defer (hydrate on hover) {
<app-child-component />
} @placeholder {
<div>Enfant</div>
}
</app-parent-component>
} @placeholder {
<div>Parent</div>
}
`
})
export class MyComponent {}Dans cet exemple :
- Si l'utilisateur survole le bloc enfant, cela déclenche l'hydratation
- Le bloc parent avec
<app-parent-component />s'hydrate d'abord - Ensuite, le bloc enfant avec
<app-child-component />s'hydrate
Bonnes pratiques
Identifier les zones à différer
Avant d'implémenter l'hydratation incrémentielle, analysez votre application pour identifier :
- Contenu critique : Doit être hydraté immédiatement (header, navigation principale)
- Contenu important mais différable : Peut être hydraté au viewport (contenu principal)
- Contenu secondaire : Peut être hydraté à l'interaction ou en idle (widgets, sidebars)
- Contenu statique : Peut ne jamais être hydraté (footers simples, textes statiques)
Choisir le bon trigger
| Trigger | Cas d'usage | Exemple |
|---|---|---|
hydrate on viewport | Contenu important qui doit être interactif dès qu'il est visible | Liste d'articles, tableau de données |
hydrate on interaction | Contenu qui nécessite une action utilisateur | Boutons, formulaires, modales |
hydrate on hover | Contenu qui réagit au survol | Tooltips, menus déroulants |
hydrate on immediate | Contenu prioritaire qui doit être hydraté dès que possible | Widgets importants mais non critiques |
hydrate on idle | Contenu secondaire non urgent | Footer, liens de navigation secondaires |
hydrate on timer | Contenu qui peut attendre un délai | Publicités, widgets tiers |
hydrate when | Contenu conditionnel | Contenu basé sur le rôle utilisateur |
hydrate never | Contenu purement statique | Textes, images sans interaction |
Tester sur différents environnements
L'hydratation incrémentielle apporte le plus de bénéfices sur :
- Appareils mobiles : CPU limité, réseau plus lent
- Réseaux lents : Réduction du JavaScript initial
- Applications complexes : Beaucoup de composants à hydrater
Testez toujours votre application dans ces conditions pour mesurer l'impact réel.
Contraintes et points d'attention
Compatibilité HTML serveur/client
Comme pour toute hydratation SSR, il est crucial que le HTML généré côté serveur corresponde exactement au HTML généré côté client. Toute divergence peut causer :
- Des erreurs de "mismatch"
- Des layout shifts (CLS)
- Des problèmes d'hydratation
ts
// ❌ ÉVITER : Divergence entre serveur et client
@Component({
template: `<div>{{ isPlatformBrowser() ? 'Client' : 'Serveur' }}</div>`
})
export class BadComponent {
isPlatformBrowser() {
return typeof window !== 'undefined';
}
}
// ✅ BON : Même rendu serveur et client
@Component({
template: `<div>{{ message }}</div>`
})
export class GoodComponent {
message = 'Contenu identique';
}