Skip to content

Vous souhaitez recevoir de l'aide sur ce sujet ? rejoignez la communauté Angular.fr sur Discord.

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.

Incremental hydration

L'hydratation incrémentielle fonctionne de la même manière :

  1. Le serveur génère tout le HTML : Tous les contenus sont déjà présents dans le HTML initial
  2. Le navigateur affiche immédiatement : L'utilisateur voit le contenu sans attendre
  3. 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 :

  1. Utilise déjà le SSR (Server-Side Rendering)
  2. 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/ssr

Activer 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 :

  1. Header : Hydraté immédiatement (critique)
  2. Liste d'utilisateurs : Hydratée au viewport
  3. Statistiques : Hydratée après interaction
  4. Footer : Hydratée en idle
  5. 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 @defer car 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 interaction s'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 idle s'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 :

  1. Si l'utilisateur survole le bloc enfant, cela déclenche l'hydratation
  2. Le bloc parent avec <app-parent-component /> s'hydrate d'abord
  3. 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

TriggerCas d'usageExemple
hydrate on viewportContenu important qui doit être interactif dès qu'il est visibleListe d'articles, tableau de données
hydrate on interactionContenu qui nécessite une action utilisateurBoutons, formulaires, modales
hydrate on hoverContenu qui réagit au survolTooltips, menus déroulants
hydrate on immediateContenu prioritaire qui doit être hydraté dès que possibleWidgets importants mais non critiques
hydrate on idleContenu secondaire non urgentFooter, liens de navigation secondaires
hydrate on timerContenu qui peut attendre un délaiPublicités, widgets tiers
hydrate whenContenu conditionnelContenu basé sur le rôle utilisateur
hydrate neverContenu purement statiqueTextes, 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';
}