AngularAtlaséd. 22.2

Chap. 12SSR et Hydration54 / 75

SSR, Prerendering et modes de rendu

Quand rendre sur le serveur, quand au build et quand uniquement dans le navigateur : RenderMode par route, app.routes.server.ts et les coûts réels.

v22architecture ~0 min de lecture Documentation officielle

Par défaut, Angular livre l’application en CSR (client-side rendering) : le serveur envoie un <app-root></app-root> vide, et le navigateur doit télécharger, analyser et exécuter tout le JavaScript — puis aussi récupérer les données — avant d’afficher quoi que ce soit d’utile. Le SSR (rendu sur le serveur, à chaque requête) et le SSG (prerender, au moment du build) déplacent le premier rendu ailleurs : l’utilisateur reçoit tout de suite du HTML avec du contenu.

Avec @angular/ssr, vous ne choisissez pas une fois pour toute l’application, mais par route : le fichier app.routes.server.ts indique pour chaque chemin s’il est en RenderMode.Prerender, RenderMode.Server ou RenderMode.Client. C’est ce qu’on appelle le hybrid rendering. L’API des routes serveur est apparue en v19 et est stable depuis la v20.

En bref
  • Le rendu dans le navigateur (CSR), sur le serveur (SSR) ou au build (prerender) se choisit par route, avec RenderMode dans app.routes.server.ts.
  • Prerender pour les pages identiques pour tous, SSR pour les pages personnalisées ou aux données qui changent souvent, CSR pour les zones derrière la connexion.
  • Le SSR améliore le premier affichage et ce que voit un robot, mais ajoute un serveur à maintenir et du code qui doit aussi tourner sur le serveur.
  • Les routes avec paramètres peuvent être prérendues si vous leur donnez la liste des valeurs, avec getPrerenderParams.
  • Le cache du CDN compte souvent plus que la vitesse du serveur.
Pourquoi c’est important

Le SSR n’est pas un interrupteur de performance gratuit. Vous gagnez en LCP, en SEO et en aperçus corrects sur les réseaux sociaux ; vous payez avec un serveur Node à exploiter, du code qui doit s’exécuter sans window, et une fenêtre pendant laquelle la page est visible mais ne répond pas encore aux clics. La bonne décision se prend par route, pas par projet.

Trois modes de rendu

CSR · ClientSSR · ServerSSG · Prerender
Le HTML apparaîtdans le navigateur, après le JSsur le serveur, à chaque requêteau moment du build, une fois
Le serveur, par requêtesert un fichier statique (le shell vide)démarre l’application, demande les données, effectue le rendusert un fichier statique
TTFBfaibleinclut le rendu et les API qu’il appelleminimal, surtout depuis un CDN
LCPaprès JS + bootstrap + requête APIdès que le HTML arrivedès que le HTML arrive
SEO, aperçusfaible : dépend du JSexcellentexcellent
Contenu par utilisateurouiouinon — HTML identique pour tout le monde
Hébergementn’importe quel hébergement statique / CDNun runtime Node (ou un autre runtime JS)n’importe quel hébergement statique / CDN
Coût par requête≈ 0CPU + mémoire pour un rendu complet≈ 0

Ce que fait concrètement le serveur en mode Server : pour chaque requête, @angular/ssr crée une nouvelle plateforme, appelle la fonction de bootstrap de main.server.ts, laisse le routeur naviguer vers l’URL, attend que l’application devienne stable (toutes les requêtes HTTP et les PendingTasks terminées), sérialise le DOM en HTML et détruit l’application. La conséquence pratique : le TTFB d’une page SSR est au moins aussi long que celui de l’API la plus lente appelée pendant le rendu.

Dans une vraie application hybride, les modes coexistent : la page d’accueil et les pages marketing sont prérendues, la recherche est rendue sur le serveur, et l’espace compte s’exécute uniquement dans le navigateur.

Ce qui se passe lors d’une requête

Cinq moments comptent, dans l’ordre où l’utilisateur les vit :

  • TTFB (time to first byte) — le premier octet de la réponse HTML.
  • FCP (first contentful paint) — le premier texte ou la première image à l’écran.
  • LCP (largest contentful paint) — l’élément principal apparaît (le titre, l’image du produit). C’est la métrique que l’utilisateur ressent comme « c’est chargé ».
  • Hydration — le JS est arrivé, Angular reprend le HTML existant et attache les listeners.
  • Interactif — un clic fait vraiment quelque chose. À partir de ce moment, l’INP compte (la rapidité de réaction de la page).

Avec le SSR et le SSG, entre le LCP et l’état « interactif », il y a une fenêtre pendant laquelle la page semble prête, mais où les boutons n’ont pas encore de listener. Sur un téléphone modeste, avec un bundle de quelques centaines de KB, cette fenêtre dure facilement 1 à 3 secondes. Lancez la simulation et comparez les trois modes sur le même réseau.

LiveLa timeline d’une requête (simulation)
magazin.ro/produse/42
Boutique
Casque sans fil Aria X2
349 RON · livraison demain
interactif
0 ms500 ms1,00 s1,50 s2,00 s
Serveur : API + rendu
HTML · 40 KB
main.js · 260 KB
Hydration
TTFB 260 msFCP = LCP 535 msinteractif 2,36 snon interactif 1,82 s

SSR : le TTFB inclut le travail du serveur (qui attend l’API), puis le contenu apparaît immédiatement. Bande jaune = contenu visible mais sans listeners jusqu’à l’hydration.

Choisissez le mode et le réseau, puis faites glisser le curseur. La bande jaune est la fenêtre « visible, mais non interactif » ; cliquez sur « Ajouter au panier » pendant ce temps. Les chiffres sont illustratifs, pas des mesures.

Remarquez ce qui ne change pas : main.js a la même taille dans les trois modes. Le SSR ne réduit pas le JavaScript, donc l’interactivité arrive à peu près au même moment qu’en CSR — parfois même un peu plus tard, car le HTML est plus gros et l’hydration consomme aussi du CPU. Le vrai gain, c’est le LCP. Pour réduire la fenêtre non interactive : moins de JS initial (routes lazy, @defer), l’incremental hydration et l’event replay, qui ne perd pas les clics faits pendant cette fenêtre.

Ce que voit un crawler

Un crawler part du HTML qu’il reçoit (ce que vous voyez avec Afficher le code source, pas dans l’onglet Éléments). En CSR, ce HTML est identique pour n’importe quelle URL. Googlebot peut exécuter du JavaScript, mais plus tard et avec des limites ; les bots qui génèrent des aperçus dans les chats et les réseaux sociaux ne lisent généralement que le HTML initial. Il y a un détail souvent ignoré : le code de statut. Un serveur statique ne sait pas que /produse/9999 n’existe pas, il répond donc 200 — un « soft 404 ».

LiveCe que reçoit un crawler : CSR, SSR ou SSG (simulation)
curl https://magazin.ro/produse/42
HTTP/1.1 200 OK<!doctype html><html lang="fr"><head>  <title>Boutique</title>  <link rel="stylesheet" href="styles-5INURTSO.css"></head><body>  <app-root></app-root>  <script src="main-HG7KQWZB.js" type="module"></script></body></html>
Aperçu dans un chat (un bot qui ne lit que le HTML)
pas d’image
magazin.ro
Boutique
— aucune description —
  • ✓Statut HTTP correct200
  • ✕<title> propre à la pageBoutique
  • ✕meta description / og:*absent
  • ✕Texte visible dans le HTML0 mot
  • ✕Liens <a href> à suivre0

Le même HTML pour chaque URL : tout le contenu n’apparaît qu’après l’exécution du JavaScript. Les bots d’aperçu ne l’exécutent pas ; Googlebot le fait, mais plus tard et avec des limites.

Changez de mode et cochez « produit inexistant ». Observez le statut, le titre, les balises meta et les marqueurs ajoutés par Angular (ngh, jsaction, ng-state).

Le titre et les balises meta se définissent avec les services Title et Meta de @angular/platform-browser. Ils passent par DOCUMENT, donc ils fonctionnent aussi sur le serveur : tout ce que vous définissez pendant le rendu se retrouve dans le HTML envoyé. Pour les pages sans données, la propriété title de la route suffit (ou votre propre TitleStrategy, si vous voulez un format commun).

import { Component, computed, effect, inject, input } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { Meta, Title } from '@angular/platform-browser';

interface Product { id: string; name: string; description: string; imageUrl: string }

@Component({
  selector: 'app-product-page',
  template: `
    @if (product.hasValue()) {
      <h1>{{ product.value().name }}</h1>
      <p>{{ product.value().description }}</p>
    }
  `,
})
export default class ProductPage {
  readonly id = input.required<string>(); // issu de la route produse/:id (withComponentInputBinding)

  private readonly title = inject(Title);
  private readonly meta = inject(Meta);
  protected readonly product = httpResource<Product>(() => `/api/products/${this.id()}`);

  constructor() {
    // S’exécute aussi sur le serveur : les balises se retrouvent dans le HTML envoyé aux crawlers.
    effect(() => {
      if (!this.product.hasValue()) return;
      const p = this.product.value();
      this.title.setTitle(`${p.name} — Boutique`);
      this.meta.updateTag({ name: 'description', content: p.description });
      this.meta.updateTag({ property: 'og:title', content: p.name });
      this.meta.updateTag({ property: 'og:description', content: p.description });
      this.meta.updateTag({ property: 'og:image', content: p.imageUrl });
    });
  }
}
Un seul endroit pour le SEO

Mettez la logique dans un service SeoService.update({ title, description, image, canonical }) et appelez-le depuis les pages de détail. Pour <link rel="canonical">, vous avez besoin de DOCUMENT — détails sur la page Code compatible serveur.

Mise en place : ce que génère ng add

# nouveau projet, avec SSR + prerender configurés dès le départ
ng new magazin --ssr

# projet existant : ajoute @angular/ssr, express et les fichiers serveur
ng add @angular/ssr

# build de production → dist/magazin/browser + dist/magazin/server
ng build

# démarre le serveur Node (script ajouté par ng add dans package.json)
npm run serve:ssr:magazin        # = node dist/magazin/server/server.mjs

La commande ajoute les dépendances (@angular/ssr, express), quatre fichiers et quelques options dans angular.json. Il vaut la peine de savoir ce que fait chacun, car vous allez les modifier :

  • server.ts — le serveur Express. Il sert les fichiers statiques de dist/…/browser et confie le reste des requêtes à AngularNodeAppEngine.handle(), qui renvoie une Response (ou null si l’URL n’est pas une route Angular). L’export reqHandler est utilisé par la CLI : ng serve effectue aussi le rendu sur le serveur.
  • main.server.ts — la fonction de bootstrap que le serveur appelle à chaque rendu.
  • app.config.server.ts — provideServerRendering(withRoutes(serverRoutes)), combiné à la config du navigateur via mergeApplicationConfig.
  • app.routes.server.ts — le render mode par route. Par défaut, tout est en Prerender.
  • app.config.ts reçoit provideClientHydration(), pour que le navigateur réutilise le HTML au lieu de le reconstruire.
import {
  AngularNodeAppEngine,
  createNodeRequestHandler,
  isMainModule,
  writeResponseToNodeResponse,
} from '@angular/ssr/node';
import express from 'express';
import { join } from 'node:path';

const browserDistFolder = join(import.meta.dirname, '../browser');

const app = express();
const angularApp = new AngularNodeAppEngine(); // une instance pour tout le serveur

// 1. (facultatif) vos API : app.get('/api/{*splat}', (req, res) => { ... });

// 2. Fichiers statiques (JS/CSS/images ont un hash dans leur nom → cache d’un an)
app.use(express.static(browserDistFolder, { maxAge: '1y', index: false, redirect: false }));

// 3. Tout le reste : Angular décide — HTML prérendu, SSR ou shell CSR
app.use((req, res, next) => {
  angularApp
    .handle(req)
    .then((response) => (response ? writeResponseToNodeResponse(response, res) : next()))
    .catch(next);
});

if (isMainModule(import.meta.url) || process.env['pm_id']) {
  const port = process.env['PORT'] || 4000;
  app.listen(port, (error) => {
    if (error) throw error;
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

// utilisé par ng serve / ng build (et par certaines plateformes serverless)
export const reqHandler = createNodeRequestHandler(app);

Un détail dans server.ts : express.static a index: false, donc même / n’est pas servi directement depuis le disque. Chaque page passe par Angular, qui applique le render mode, les headers et le statut configurés pour la route.

allowedHosts en production (v22)

Pour prévenir le SSRF, le serveur valide l’en-tête Host. Depuis la v22, un host inconnu reçoit 400 Bad Request (avant, le serveur basculait en CSR). ng add met "allowedHosts": [] : renseignez vos domaines dans angular.json (les jokers comme *.magazin.ro sont acceptés), dans les options de AngularNodeAppEngine ou dans la variable d’environnement NG_ALLOWED_HOSTS. Derrière un load balancer, les en-têtes X-Forwarded-* sont ignorés tant que vous ne les autorisez pas explicitement avec trustProxyHeaders. Détails dans Sécurité.

Si vous avez un projet plus ancien dont le server.ts utilise CommonEngine : cette API est dépréciée depuis la v22, son remplaçant étant AngularNodeAppEngine (Node) ou AngularAppEngine (runtimes basés sur les Request/Response de la Web API).

RenderMode par route

import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  // Identique pour tous, ne change qu’au déploiement → HTML généré au build (SSG)
  { path: '', renderMode: RenderMode.Prerender },
  { path: 'despre', renderMode: RenderMode.Prerender },

  // Public, indexé, données qui changent souvent → rendu à chaque requête (SSR)
  { path: 'cautare', renderMode: RenderMode.Server },

  // Derrière la connexion : pas de SEO, HTML personnel → navigateur uniquement (CSR)
  { path: 'cont/**', renderMode: RenderMode.Client },

  // Le filet de sécurité pour toute route Angular non listée ci-dessus
  { path: '**', renderMode: RenderMode.Server },
];

Les règles qui comptent en pratique :

  • Correspondance : un segment exact l’emporte sur un paramètre (:id), qui l’emporte sur **. Donc ** est le filet de sécurité, pas une règle qui « écrase ».
  • Validation dans les deux sens : chaque route du routeur doit correspondre à une route serveur, et chaque route serveur (sauf celles qui se terminent par /**) doit exister dans le routeur. Sinon, le build s’arrête avec un message explicite.
  • Le chemin ne commence pas par « / » — '/despre' est une erreur de build.
  • Ce que reçoit le navigateur : Prerender → le fichier généré au build (avec un ETag) ; Server → le HTML rendu maintenant ; Client → index.csr.html, le shell vide, lui aussi servi par votre serveur.
LiveQuel render mode pour cette route ? (simulation)
Exemples :
RecommandationRenderMode.PrerenderSSG au build + SSR pour le reste
  • Le HTML est identique pour tout le monde et stable : vous le générez une fois, au moment du build.
  • Servi comme un fichier statique (idéalement depuis un CDN) : TTFB minimal, coût par requête quasi nul.
Points d’attention
  • Les données changent chaque jour → il vous faut un build planifié (p. ex. chaque nuit en CI) qui régénère les pages.
  • Des milliers de pages ajoutent des minutes au build ; mesurez le temps de build et la taille du déploiement.
import { PrerenderFallback, RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  {
    path: 'produse/:id',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const api = inject(CatalogApi); // synchrone, avant le await
      const items = await api.mostVisited(500);
      return items.map((x) => ({ id: String(x.id) }));
    },
    fallback: PrerenderFallback.Server,
  },
  // ...
];
Essayez les exemples ou décrivez votre propre route. La recommandation et l’entrée dans app.routes.server.ts se mettent à jour en direct ; « Points d’attention » montre le coût caché du choix.

Headers, statut et redirections

Chaque ServerRoute peut avoir des headers et un status. L’exception : les routes Prerender acceptent headers, mais pas status — le type ServerRoutePrerender l’exclut, car le fichier n’est généré qu’une seule fois. Les headers d’une page prérendue ne s’appliquent que lorsqu’elle est servie par le serveur Node, pas depuis un hébergement statique.

import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  {
    // HTML public : le CDN peut le garder 60 s et servir l’ancienne version pendant qu’il se rafraîchit
    path: 'cautare',
    renderMode: RenderMode.Server,
    headers: { 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300' },
  },
  {
    // redirection permanente : { path: 'oferte', redirectTo: 'promotii' } dans app.routes.ts
    path: 'oferte',
    renderMode: RenderMode.Server,
    status: 301, // sans status : 302
  },
  {
    // prerender : headers oui, status NON (le type ServerRoutePrerender l’exclut)
    path: 'despre',
    renderMode: RenderMode.Prerender,
    headers: { 'X-Frame-Options': 'DENY' },
  },
  { path: '**', renderMode: RenderMode.Server },
];

Un redirectTo du routeur devient une vraie redirection HTTP sur le serveur (302 par défaut ; vous pouvez définir status: 301, 303, 307 ou 308), renvoyée sans rendre l’application. De même, si un guard redirige pendant le rendu (voir Guards & resolvers), l’URL finale diffère de l’URL demandée et le serveur répond par une redirection. Avec outputMode: 'static', où il n’y a pas de serveur, les redirections sont prérendues sous forme de « soft redirects » avec un <meta http-equiv="refresh"> dans le HTML.

Statut et headers décidés à l’exécution

Quand le statut dépend des données (le produit n’existe pas), vous le définissez depuis le composant via RESPONSE_INIT. Pour les cookies, la langue ou le user agent, vous lisez la requête via REQUEST. Les deux viennent de @angular/core et valent null dans le navigateur, au prerender et au build — gérez toujours le cas null.

import { Component, RESPONSE_INIT, inject } from '@angular/core';

@Component({
  selector: 'app-not-found',
  template: `<h1>Page introuvable</h1>`,
})
export default class NotFound {
  constructor() {
    // RESPONSE_INIT vaut null dans le navigateur, au prerender et au build → toujours vérifier.
    const response = inject(RESPONSE_INIT);
    if (response) response.status = 404; // le crawler voit 404, et non « 200 avec un texte d’erreur »
  }
}

Prerender avec paramètres

Les routes sans paramètres sont découvertes et prérendues automatiquement. Pour produse/:id, Angular ne peut pas deviner quels IDs existent, vous lui fournissez donc une fonction getPrerenderParams. Elle s’exécute au moment du build, dans un contexte d’injection (vous pouvez utiliser inject()), et renvoie une liste d’objets paramètre → valeur. Une route Prerender avec des paramètres mais sans getPrerenderParams arrête le build.

import { inject } from '@angular/core';
import { PrerenderFallback, RenderMode, ServerRoute } from '@angular/ssr';
import { ProductApi } from './products/product-api';

export const serverRoutes: ServerRoute[] = [
  {
    path: 'produse/:id',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const api = inject(ProductApi); // ✓ inject() synchrone, AVANT tout await
      const ids = await api.topProductIds(500); // s’exécute au BUILD, pas à la requête
      return ids.map((id) => ({ id: String(id) })); // les valeurs doivent être des chaînes
    },
    // /produse/9999 n’a pas été prérendu → que fait-on à la requête ?
    fallback: PrerenderFallback.Server, // par défaut : SSR à la requête
  },
  {
    path: 'blog/:slug/**', // paramètres + catch-all
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      return [{ slug: 'ssr', '**': 'partea/1' }]; // → /blog/ssr/partea/1
    },
  },
  { path: '**', renderMode: RenderMode.Server },
];

Ce qui arrive à une URL qui n’a pas été générée au build dépend de fallback :

PrerenderFallbackPour /produse/9999 (non prérendu)Quand le choisir
Server (par défaut)rendu à la demande, sur le serveurun grand catalogue : prérendre le top N, le reste à la demande
Clientreçoit le shell CSR, rendu dans le navigateurvous voulez du SEO uniquement pour les pages générées et ne voulez pas de SSR
NoneAngular ne répond pas (la requête continue, en général un 404)la liste issue du build est complète par définition
inject() après await

inject() ne fonctionne que de façon synchrone, au début de la fonction. Un inject() écrit après un await n’est plus dans un contexte d’injection et lève une erreur. Récupérez toutes les dépendances dans les premières lignes, puis faites les requêtes.

Le coût est au moment du build : chaque URL est un rendu complet. Dix mille produits, c’est dix mille rendus à chaque déploiement et dix mille fichiers à téléverser. Si les données changent chaque jour, il faut aussi un build planifié qui les régénère.

Déploiement, cache et coûts

outputMode dans angular.json décide de ce que produit le build. Avec "server" (ce que définit ng add), vous obtenez dist/app/browser plus dist/app/server/server.mjs, et il vous faut un processus Node. Avec "static", vous n’obtenez que des fichiers : les pages prérendues et le shell CSR, déployables sur n’importe quel CDN. Une route RenderMode.Server en mode static est une erreur de build.

{
  "projects": {
    "magazin": {
      "architect": {
        "build": {
          "options": {
            "outputMode": "static"
          }
        }
      }
    }
  }
}

Les couches de cache

QuoiCommentAttention
JS, CSS, images avec un hashmaxAge: '1y' dans express.static / CDNles noms changent à chaque build, donc un cache long est sûr
HTML prérendufichier statique, ETag → 304 Not Modifiedne change qu’au déploiement
HTML SSR publicheaders par route : public, s-maxage=60, stale-while-revalidate=300uniquement pour du contenu identique pour tout le monde
HTML SSR personnelprivate, no-storejamais public : le CDN le servirait à un autre utilisateur
Données HTTP pendant le rendutransfer cache : les réponses GET/HEAD finissent dans le HTML et sont réutilisées à l’hydrationles requêtes avec Authorization/Cookie sont exclues par défaut

Le transfer cache est la raison pour laquelle, dans la timeline, le SSR ne refait plus la requête API depuis le navigateur : les données récupérées par HttpClient sur le serveur sont sérialisées dans <script id="ng-state"> et lues au démarrage. Ses options (withHttpTransferCacheOptions, withNoHttpTransferCache) se trouvent sur la page Hydration. Aussi : sur le serveur, avec le backend fetch par défaut, chaque réponse HTTP est limitée à 1 MB (erreur NG02825) ; la limite se modifie avec provideServerRendering({ maxResponseBodySize }, …).

Coût opérationnel

  • CPU par requête : chaque rendu SSR est un bootstrap complet. Sans cache CDN, le trafic des bots se paie en serveurs.
  • TTFB lié au backend : le serveur attend la stabilité, donc une API lente ralentit toutes vos pages. Mettez en place des timeouts et déplacez les données secondaires dans @defer (sur le serveur, un bloc sans trigger hydrate ne rend que le placeholder).
  • Mémoire partagée entre les requêtes : le code est évalué une fois par processus, donc les variables au niveau du module et les providers useValue sont communs à tous les utilisateurs.
  • Un système de plus en production : logs, monitoring, scaling, version de Node, cold start sur serverless.
import { InjectionToken } from '@angular/core';

// ✗ État au niveau du module : sur le serveur, le module est évalué UNE SEULE fois,
//    donc la variable est partagée par toutes les requêtes (et tous les utilisateurs).
let currentUser: string | null = null;

// ✗ useValue au niveau de l’application : la même valeur jusqu’au redémarrage du serveur.
export const RENDERED_AT = new InjectionToken<number>('RENDERED_AT');
const bad = { provide: RENDERED_AT, useValue: Date.now() };

// ✓ useFactory : s’exécute à chaque rendu (chaque requête a son propre injecteur).
const good = { provide: RENDERED_AT, useFactory: () => Date.now() };

// ✓ L’état par utilisateur vit dans des services (instanciés à chaque requête), pas dans des variables globales.

Quand OUI, quand NON

SSR / SSG en valent la peine
  • Pages publiques qui doivent être indexées et partagées : produits, articles, landing pages. Prerender si vous pouvez les générer au build, Server sinon.
  • Le LCP se traduit en argent (e-commerce, médias), et certains utilisateurs ont des téléphones et des réseaux modestes.
  • Contenu stable (documentation, blog, marketing) : Prerender + outputMode: 'static', sans serveur à maintenir.
  • Contenu public avec données fraîches et fort trafic : Server + un cache court au niveau du CDN.
  • Vous avez besoin de codes de statut corrects (404, 301) pour le SEO.
Rester en CSR (ou choisir autre chose)
  • Applications derrière une connexion (dashboard, admin, back-office) : Client. Investissez dans un petit bundle, des routes lazy et @defer.
  • Vous n’avez personne pour exploiter un serveur Node : restez en CSR ou en Prerender avec outputMode: 'static'.
  • « L’application est lente après le chargement » : le SSR n’aide pas après l’hydration. Le problème est dans le JS ou dans la change detection → Performance, Change detection.
  • Routes construites sur des API du navigateur (cartes, canvas, éditeurs) : RenderMode.Client sur cette route.
  • HTML personnalisé avec fort trafic : ne faites pas de rendu par utilisateur. Prérendez la partie commune et chargez les éléments personnels dans le navigateur.

Pièges fréquents

window is not defined

Le code qui touche à window, document ou localStorage dans le constructeur, dans ngOnInit ou au niveau du module plante sur le serveur. La solution : afterNextRender, le token DOCUMENT et des implémentations de service distinctes pour le serveur et le navigateur — le tout sur la page Code compatible serveur.

Données mélangées entre utilisateurs

Une variable let currentUser au niveau du module, ou un useValue calculé au démarrage, vit aussi longtemps que le processus Node, pas aussi longtemps que la requête. Mettez l’état dans des services (instanciés à chaque rendu) et utilisez useFactory pour les valeurs calculées par requête.

  • Cache public sur du HTML personnalisé → un utilisateur voit la page d’un autre. Sur les routes Server avec des données personnelles, utilisez Cache-Control: private, no-store.
  • allowedHosts vide en production → 400 sur toutes les pages (v22). Configurez les domaines ou NG_ALLOWED_HOSTS.
  • « Soft 404 » : le composant « not found » répond 200. Définissez status = 404 via RESPONSE_INIT.
  • Mauvais fallback SPA sur un hébergement statique : avec le prerender, index.html est la page de la route /. Les routes Client doivent retomber sur index.csr.html.
  • L’application ne devient pas stable : une PendingTask sans nettoyage (ou, avec Zone.js, un setInterval lancé dans la zone Angular) fait attendre le rendu, et la requête ne se termine jamais.
  • Un CDN qui minifie le HTML et supprime les commentaires → l’hydration échoue avec NG0507. Les commentaires d’Angular sont nécessaires ; désactivez cette option sur le CDN.
  • « J’ai ajouté du SSR et le TTI n’a pas bougé » — c’est normal : le JS est le même. Mesurez le LCP, pas le TTI, et réduisez le JS initial séparément.

Aide-mémoire

// ng new app --ssr  |  ng add @angular/ssr  |  ng build  |  node dist/app/server/server.mjs

// app.config.server.ts
provideServerRendering(withRoutes(serverRoutes));
provideServerRendering({ maxResponseBodySize: 5 * 1024 * 1024 }, withRoutes(serverRoutes)); // 1 MB par défaut

// app.config.ts (browser)
provideClientHydration();

// app.routes.server.ts — import { RenderMode, PrerenderFallback, ServerRoute } from '@angular/ssr'
export const serverRoutes: ServerRoute[] = [
  { path: 'despre', renderMode: RenderMode.Prerender }, // HTML au build (SSG)
  {
    path: 'produse/:id',
    renderMode: RenderMode.Prerender,
    getPrerenderParams: async () => [{ id: '1' }, { id: '2' }], // s’exécute au build
    fallback: PrerenderFallback.Server, // | Client | None — Server par défaut
  },
  { path: 'cautare', renderMode: RenderMode.Server, headers: { 'Cache-Control': 'no-store' } },
  { path: 'cont/**', renderMode: RenderMode.Client }, // shell vide, rendu dans le navigateur
  { path: '**', renderMode: RenderMode.Server }, // le filet de sécurité
];

// @angular/core — tous null dans le navigateur, au prerender et au build
inject(REQUEST); // Request : headers, cookies, URL
inject(RESPONSE_INIT); // ResponseInit : status / headers définis dynamiquement
inject(REQUEST_CONTEXT); // le deuxième argument de angularApp.handle(req, context)

// angular.json → architect.build.options
//   "outputMode": "server" | "static"               static = pas de Node : uniquement Prerender + Client
//   "security": { "allowedHosts": ["exemplu.ro"] }  host inconnu → 400 Bad Request (v22)

Chap. 12 · 0 sur 3 terminées

Leçon terminée ?

Marquez-la : elle apparaîtra sur votre carte, dans le sommaire et sur la page d’accueil.

Se marque toute seule une fois la leçon lue en entier.

  1. SSR, Prerendering et modes de rendu
  2. Hydration : event replay et incrémentale
  3. Code compatible serveur
Voir toute votre carte

Les 75 pages · tapez pour filtrer

  • 01Nouveautés d’Angular 21 & 22Tous les changements importants de la v21 et de la v22, en bref, avec un lien vers la page qui explique chacun.Départ
  • 01Votre première application, pas à pasUne liste de lecture construite d’un composant vide jusqu’à une application terminée — signals, @for, inputs et outputs, un service et un formulaire, pas à pas.Départ
  • 01Décisions rapides« Où mettre ça ? », « Signal ou Observable ? », « Shared ou feature ? » — des réponses courtes aux questions de tous les jours.Départ
  • 01Questions d’entretien Angular60 vraies questions d’entretien pour profils junior, confirmé et senior, avec la réponse à donner, la relance et les signaux d’alerte. Plus un entretien blanc.Départ
  • 02Standalone et bootstrapComment une application démarre sans NgModule : bootstrapApplication, app.config.ts, imports dans le composant.Fondamentaux modernes
  • 02Data binding et événementsInterpolation, bindings de propriétés et d’attributs, événements avec $event et filtres de touches, et comment [( )] est construit — ce qui entre dans le DOM et ce qui en revient.Fondamentaux modernes
  • 02Control Flow : @if, @for, @switchLa syntaxe de blocs qui remplace *ngIf/*ngFor/ngSwitch, avec track, @empty et vérifications d’exhaustivité.Fondamentaux modernes
  • 02@let et les nouvelles expressions de templateVariables locales avec @let, fonctions fléchées, spread, regex, instanceof, commentaires dans les balises.Fondamentaux modernes
  • 02@defer — Lazy Loading dans les templatesChargez les composants lourds uniquement quand c’est nécessaire : triggers, prefetch, placeholder, loading, error.Fondamentaux modernes
  • 02Bindings de classe et de stylePourquoi [class.x] et [style.x] ont remplacé ngClass et ngStyle (avec migration automatique depuis la v21).Fondamentaux modernes
  • 02Styles des composants et encapsulation:host, les trois modes d’encapsulation, les thèmes avec des variables CSS, et pourquoi ::ng-deep est un dernier recours.Fondamentaux modernes
  • 02Pipes : quand et commentPipes pures vs impures, pipes personnalisées, et comment choisir entre une pipe, computed() et une méthode.Fondamentaux modernes
  • 03signal() et computed()Les primitives réactives : état modifiable, état dérivé, égalité, untracked et règles d’immutabilité.Signals et réactivité
  • 03effect() — quand OUI, quand NONEffets de bord synchronisés avec les Signals : localStorage, logs, API impératives. Et leurs pièges.Signals et réactivité
  • 03linkedSignal() — état dérivé modifiableUn signal qui se réinitialise quand sa source change, mais que vous pouvez aussi modifier à la main.Signals et réactivité
  • 03resource, rxResource, httpResourceDes données asynchrones sous forme de Signals : status, value, error, reload, annulation automatique. Stable depuis la v22.Signals et réactivité
  • 03debounced() — signals différésLa version « debounce » d’un signal, sans RxJS. Parfait pour la recherche.Signals et réactivité
  • 03Listes : rechercher, filtrer, trier, paginerUne chaîne de signals computed qui ne recalcule que ce qui a changé, avec les filtres dans l’URL pour qu’un lien rouvre la même vue.Signals et réactivité
  • 03Les Observables depuis zéroCe qu’est un Observable, subscribe et unsubscribe, les opérateurs que vous utilisez chaque semaine, froid ou chaud — et quand un signal suffit.Signals et réactivité
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — et quand RxJS reste le bon choix.Signals et réactivité
  • 04input() et output()Les inputs comme Signals (required, transform, alias) et les outputs typés. Les données descendent, les événements remontent.Composants : communication
  • 04model() — binding bidirectionnelLe « banana-in-a-box » [( )] avec une seule API. Quand l’utiliser, et quand input + output est préférable.Composants : communication
  • 04viewChild et contentChildLes queries comme Signals : viewChild, viewChildren, contentChild, contentChildren, l’option read.Composants : communication
  • 04Lifecycle et DestroyRefQuels hooks comptent encore à l’ère des Signals, afterNextRender, DestroyRef et ngOnChanges typé.Composants : communication
  • 04Projection de contenung-content avec select, plusieurs slots, contenu par défaut et le piège de la projection conditionnelle.Composants : communication
  • 04ng-template et ngTemplateOutletLes templates comme valeurs : les render props en Angular, contexte typé et personnalisation des composants.Composants : communication
  • 05Smart vs DumbContainer vs présentation : qui détient l’état, qui se contente d’afficher, et comment bien les séparer.Conception des composants
  • 05Composants génériques <T>Un DataTable<T> ou un Select<T> correctement typé, avec des templates pour les cellules.Conception des composants
  • 05Composants dynamiquesNgComponentOutlet, createComponent avec inputBinding/outputBinding, et un tableau de bord construit à partir d’une configuration.Conception des composants
  • 05Directives et composition d’hôteLes directives d’attribut, les métadonnées host: {} et la Directive Composition API (hostDirectives).Conception des composants
  • 05Contrôles de formulaire personnalisésFormValueControl (Signal Forms) vs ControlValueAccessor : une notation par étoiles, dans les deux variantes.Conception des composants
  • 05Change DetectionOnPush est la valeur par défaut depuis la v22, Zone.js a disparu depuis la v21. Ce qui déclenche le rendu et comment éviter les mauvaises surprises.Conception des composants
  • 06inject() et @Service()Le nouveau décorateur @Service() de la v22, inject() vs le constructeur, et le contexte d’injection.Dependency Injection
  • 06Providers et hiérarchie des injecteursRoot vs route vs composant, useClass/useValue/useFactory/useExisting, et les modificateurs optional/self/skipSelf.Dependency Injection
  • 06InjectionToken et provideX()Configuration typée, fonctions provideFeature(), makeEnvironmentProviders et initializers.Dependency Injection
  • 06injectAsync() — services en Lazy LoadingChargez un service lourd uniquement quand vous en avez besoin, avec prefetch à l’idle.Dependency Injection
  • 07Structure des dossierscore / shared / features, ce que contient chacun, et à quoi ressemble l’intérieur d’une feature.Architecture d’application
  • 07Shared vs featureQuand déplacer un composant dans shared et (surtout) quand NE PAS le faire. Une checklist concrète.Architecture d’application
  • 07Interfaces et modèlesInterface en ligne ou fichier séparé ? DTO vs modèle de domaine, type vs interface, où les ranger.Architecture d’application
  • 07State Management avec les SignalsStores simples à base de Signals, facades, état local vs global, et quand NgRx devient nécessaire.Architecture d’application
  • 07Guide de style et nommageLes conventions officielles actuelles : noms de fichiers sans suffixes, protected, readonly, ordre des membres.Architecture d’application
  • 07Barrels, alias et frontièresindex.ts, alias de chemins, API publique d’une feature et règles de dépendances vérifiables automatiquement.Architecture d’application
  • 07Erreurs : @boundary et ErrorHandlerLe nouveau bloc @boundary de la v22.2, l’ErrorHandler global, les erreurs HTTP et les erreurs de resource.Architecture d’application
  • 08Routing moderne et Lazy LoadingloadComponent, routes enfants, paramètres comme inputs et héritage des paramètres (le nouveau comportement par défaut de la v22).Routing
  • 08Guards et resolverscanActivate, canMatch, canDeactivate et resolve sous forme de fonctions, avec redirections via UrlTree / RedirectCommand.Routing
  • 09Signal Forms — les basesDes formulaires bâtis sur les Signals : form(), [formField], validation par schéma. Stable depuis la v22.Formulaires
  • 09Signal Forms — avancéValidation personnalisée et inter-champs, async/HTTP avec debounce, listes dynamiques, désactivé/masqué, schémas réutilisables.Formulaires
  • 09Reactive Forms typésFormGroup typé, nonNullable, FormArray — et leur cohabitation avec les Signal Forms.Formulaires
  • 10HttpClient, interceptors et couche d’APIHttpClient sans configuration (v21), Fetch par défaut (v22), interceptors fonctionnels et une couche d’API avec mapping des DTO.HTTP et données
  • 11Performance — une checklistCe qui compte vraiment : OnPush + Signals, track, @defer, routes en Lazy Loading, images, bundles et Hydration.Qualité et performance
  • 11Tests avec VitestVitest est le runner par défaut depuis la v21. TestBed, composants avec Signals, services, HTTP et harnesses.Qualité et performance
  • 11DevTools : regarder dans votre applicationChrome DevTools et Angular DevTools pas à pas : ce que montre chaque panneau, avec des exercices qui fonctionnent sur ce site.Qualité et performance
  • 11Angular AriaDes composants headless et accessibles que vous stylez comme vous voulez : tabs, accordion, menu, listbox...Qualité et performance
  • 12SSR, Prerendering et modes de renduQuand rendre sur le serveur, quand au build et quand uniquement dans le navigateur : RenderMode par route, app.routes.server.ts et les coûts réels.SSR et Hydration
  • 12Hydration : event replay et incrémentaleComment le navigateur reprend le HTML du serveur sans le re-rendre : Hydration complète, event replay, @defer (hydrate on …) et erreurs de mismatch.SSR et Hydration
  • 12Code compatible serveurwindow n’existe pas sur le serveur : PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, le cache HTTP et PendingTasks.SSR et Hydration
  • 13Micro frontendsQuand découper une application en plusieurs déploiements vaut le coup, et quand c’est juste de la complexité : Native Federation, Module Federation, iframes, Web Components.Architecture à grande échelle
  • 13Web Components avec Angular ElementscreateCustomElement : un composant Angular utilisé comme balise HTML n’importe où, plus la communication entre applications via des événements et BroadcastChannel.Architecture à grande échelle
  • 13Monorepo, bibliothèques et frontièresUn workspace avec plusieurs applications et bibliothèques : ng generate library, Nx, points d’entrée secondaires, publication et règles de dépendances.Architecture à grande échelle
  • 13Architecture hexagonale (ports et adapters)Un domaine sans Angular, des ports sous forme de classes abstraites et des adapters interchangeables : HTTP, localStorage ou en mémoire, sans toucher à l’UI.Architecture à grande échelle
  • 14Les Signals sous le capotLe graphe réactif : producteurs et consommateurs, push-pull, pourquoi computed est lazy et sans glitch, le problème du diamant et quand les effects s’exécutent.Réactivité avancée
  • 14RxJS avancé : opérateurs d’ordre supérieurswitchMap, mergeMap, concatMap et exhaustMap sur un diagramme interactif, plus retry avec backoff, shareReplay et comment éviter les fuites.Réactivité avancée
  • 14NgRx SignalStoreState Management à grande échelle sur les Signals : withState, withComputed, withMethods, rxMethod, entités et hooks — et quand un store simple suffit.Réactivité avancée
  • 14Machines à états avec les signalsFini les états impossibles : une machine typée sur des signals, avec transitions, gardes et un diagramme d’états en direct.Réactivité avancée
  • 15WebSockets : un temps réel qui résiste au réseauUn socket typé sous forme de signals : reconnexion avec backoff et jitter, heartbeat, file d’attente hors ligne et sécurité SSR.Temps réel et hors ligne
  • 15Mises à jour optimistes et rollbackUne UI instantanée qui reste honnête : snapshot et rollback, réponses dans le désordre, clés d’idempotence.Temps réel et hors ligne
  • 15Offline-first : Service Worker et cacheLe Service Worker d’Angular : stratégies de cache, annonce des nouvelles versions et file d’attente pour les modifications faites hors ligne.Temps réel et hors ligne
  • 16Web WorkersDes calculs lourds sans bloquer l’UI : un vrai worker dans la page, l’échange de messages et quand ça n’en vaut pas la peine.UI avancée et performance
  • 16Listes géantes : virtual scroll100 000 lignes sans latence : le virtual scroll du CDK, itemSize, trackBy et les alternatives (pagination, scroll infini).UI avancée et performance
  • 16Animations modernes et View Transitionsanimate.enter et animate.leave à la place de @angular/animations (déprécié), transitions CSS et View Transitions à la navigation.UI avancée et performance
  • 16Fuites mémoire : les trouver et les corrigerAbonnements, timers et listeners qui survivent à leur composant — comment les repérer dans les DevTools et les fermer.UI avancée et performance
  • 17Sécurité : XSS, sanitisation et CSPCe qu’Angular fait automatiquement contre le XSS, quand bypassSecurityTrust devient dangereux, plus la CSP avec nonce, Trusted Types et XSRF.Sécurité et internationalisation
  • 17Authentification : tokens et refreshUn interceptor qui ajoute le token, le renouvelle une seule fois sur une 401 et rejoue les requêtes en attente — plus où conserver le token.Sécurité et internationalisation
  • 17OAuth 2.1 et OpenID Connect avec PKCELe flux authorization code avec PKCE, pas à pas : verifier et challenge, state et nonce, l’ID token, et pourquoi un BFF est plus sûr.Sécurité et internationalisation
  • 17Internationalisation (i18n)$localize, l’attribut i18n, ICU plural/select, un build par langue vs des traductions à l’exécution, et le formatage selon la locale.Sécurité et internationalisation