AngularAtlasAusg. 22.2

Kap. 12SSR & Hydration56 / 75

Servertauglicher Code

window gibt es auf dem Server nicht: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, der HTTP-Cache und PendingTasks.

v22 ~0 Min. Lesezeit Offizielle Doku

Bei SSR oder Prerendering laufen deine Komponenten zweimal: einmal in Node — bei jedem Request oder zur Build-Zeit — um HTML zu erzeugen, und noch einmal im Browser, während der Hydration. Konstruktoren, Feld-Initialisierungen, ngOnInit, effect(), Services, Interceptors und Guards laufen auf beiden Plattformen. Nur wenige APIs sind dem Browser vorbehalten: afterNextRender, afterEveryRender, afterRenderEffect und natürlich die Events des Nutzers.

Auf dem Server gibt es kein window, kein localStorage, kein Layout und keine Observer. Es gibt ein Dokument — von Domino emuliert — aber nur über inject(DOCUMENT). Code, der „für den Browser“ geschrieben wurde, funktioniert in ng serve ohne SSR einwandfrei und scheitert erst im Server-Build. Oder, schlimmer, er scheitert nicht, sondern liefert falsches HTML: das falsche Theme, fehlende Daten, jeder Request doppelt ausgeführt.

Kurz gesagt
  • Auf dem Server gibt es kein window, document oder localStorage. Code, der sie braucht, läuft in afterNextRender.
  • Was du auf dem Server geladen hast, kommt über den HTTP-Transfer-Cache in den Browser, sodass HttpClient den Request bei der Hydration nicht wiederholt.
  • Der Server schickt die Seite erst, wenn die App keine offenen Tasks mehr hat (PendingTasks). Ein Timer, der nie stoppt, hält den Request offen.
  • Eine nicht existierende Seite muss mit Status 404 antworten, nicht nur „nicht gefunden“ anzeigen.
  • Variablen auf Modulebene teilen sich alle Requests auf dem Server. Der Zustand eines Nutzers gehört in Services, nicht dorthin.
Warum das wichtig ist
Diese Seite ist die Checkliste für Code-Reviews in einem Projekt mit SSR: was auf dem Server kaputtgeht, wohin du den Browser-Code verschiebst, wie du Arbeit nicht doppelt erledigst (TransferState und der HTTP-Cache), wann der Server die Seite für fertig hält (PendingTasks) und wie du verhinderst, dass Zustand zwischen Nutzern durchsickert. Die SSR-Konfiguration steht in SSR, Prerendering & Render Modes, und die Übernahme des HTML im Browser in Hydration.

Was es auf dem Server nicht gibt

Der Angular-Server läuft in Node, mit einem von Domino emulierten DOM (enthalten in @angular/platform-server). Der wichtige Unterschied: Die Globals des Browsers fehlen komplett und lösen einen ReferenceError aus, während das per DI bezogene Dokument zwar eine Struktur, aber kein Layout hat. Die Tabelle wurde mit Node 22/24 und mit dem Domino aus @angular/platform-server 22.2.2 verifiziert.

APIAuf dem ServerStattdessen verwenden
window, document (global), location, historyexistiert nicht: ReferenceError: window is not definedinject(DOCUMENT), Router, Location
localStorage, sessionStorageexistieren nichtafterNextRender; für alles, was beim Rendern zählt, ein Cookie aus REQUEST
matchMedia, innerWidthfehlen auch auf document.defaultView (dem Domino-Window)CSS-Media-Queries; BreakpointObserver aus dem CDK (auf dem Server treffen die Queries nicht zu); afterNextRender
IntersectionObserver, ResizeObserver, MutationObserver, requestAnimationFrameexistieren nicht@defer (on viewport); afterNextRender + DestroyRef
getBoundingClientRect(), offsetWidth, scrollHeightDomino-Elemente haben sie nicht (TypeError oder undefined)afterNextRender({ read })
document.cookieder Getter wirft Error: NotYetImplementedinject(REQUEST)?.headers.get('cookie')
navigatorexistiert ab Node 21+, gehört aber dem Server (userAgent: 'Node.js/22')die Header aus REQUEST; isPlatformBrowser
setTimeout, setInterval, fetchexistieren, aber Timer stoppen nicht von selbst, und fetch akzeptiert keine relativen URLsafterNextRender + DestroyRef; HttpClient

Was wo läuft

CodeServerBrowser
Feld-Initialisierungen, Konstruktor, ngOnInit, ngOnChangesjaja
effect(), computed(), das Template, Pipesjaja
Services, Interceptors, Guards, Resolverja — bei jedem Request neue Instanzenja
ngOnDestroy, DestroyRef.onDestroyja — der Server zerstört die Anwendung nach dem Rendernja
afterNextRender, afterEveryRender, afterRenderEffectnein (NOOP)ja
Event-Listener: (click), (input)…nein — auf dem Server klickt niemandja
@defer ohne hydratenur der @placeholderhier starten die Trigger
LiveLäuft das auf dem Server? (Simulation der Server-Umgebung)
6 Fehler2 Warnungen3 OK
theme = 'light';

constructor() {
  this.theme = localStorage.getItem('theme') ?? 'light';
}
Fehler · Server (Node + Domino)ReferenceError: localStorage is not defined

Warum: Der Konstruktor läuft auch auf dem Server, und in Node gibt es kein localStorage. Selbst wenn es eines gäbe, hätte der Server keinen Zugriff auf den Storage des Nutzers.

Die Lösung
readonly theme = signal('light');
constructor() {
  afterNextRender(() =>
    this.theme.set(localStorage.getItem('theme') ?? 'light'));
}
// oder: ein auf dem Server aus inject(REQUEST) gelesenes Cookie
Wechsle zwischen Browser und „Läuft auf dem Server“. Jedes Fragment läuft wirklich: auf „Server“ in einer Sandbox, in der die Browser-Globals wie in Node fehlen, navigator der von Node ist und inject(DOCUMENT) ein Dokument im Domino-Stil liefert. Die Fehlermeldungen sind dieselben wie in Node. Nur die TypeScript-Modifier (private, readonly) werden vor der Ausführung entfernt.
Die Regel
Keine Browser-API im Konstruktor, in Feld-Initialisierungen, in ngOnInit oder in effect(). Alles, was das DOM, das Layout, den Storage oder eine Browser-Bibliothek berührt, gehört in afterNextRender / afterRenderEffect — oder hinter einen Service mit separater Server-Implementierung.

afterNextRender und die Phasen

afterNextRender und afterEveryRender (stabil seit v20, als afterRender in afterEveryRender umbenannt wurde) registrieren Callbacks, die laufen, nachdem Angular das Rendern abgeschlossen hat — nur im Browser. Auf dem Server sind sie NOOPs: Der Callback wird nicht einmal registriert. Deshalb sind sie der Standardort für jeden Code, der das DOM, das Layout oder die Window-APIs berührt. Sie müssen in einem Injection Context (Konstruktor, Feld-Initialisierung) oder mit der Option injector aufgerufen werden, und sie räumen von selbst auf, wenn die Komponente zerstört wird.

Du kannst ihnen eine Funktion oder ein Objekt mit Phasen übergeben. Die Phasen laufen in der Reihenfolge earlyRead → write → mixedReadWrite → read, und jede erhält den Wert, den die vorherige Phase zurückgibt. Die Trennung gibt es, damit Angular die Lese- und Schreibzugriffe der ganzen Anwendung bündeln kann: Ein Layout-Lesezugriff nach einem Schreibzugriff zwingt den Browser, das Layout sofort neu zu berechnen (Layout Thrashing). Eine einfache Funktion läuft in mixedReadWrite, der Phase, die du laut Dokumentation vermeiden sollst.

PhaseErlaubtBeispiel
earlyReadnur Lesen, vor den Schreibzugriffendu misst etwas, wovon ein Schreibzugriff abhängt (eigenes Layout)
writenur SchreibenStyles, Initialisierung einer Bibliothek
mixedReadWriteLesen und Schreiben gemischtnur, wenn sich die Arbeit nicht aufteilen lässt
readnur Lesen, nach den Schreibzugriffendu misst das Endergebnis und legst es in einem Signal ab
import { Component, ElementRef, afterNextRender, input, viewChild } from '@angular/core';

/** Verkleinert den Titel, bis er in eine Zeile passt. Auf dem Server passiert nichts. */
@Component({
  selector: 'app-fit-title',
  template: `<h2 #title class="fit">{{ text() }}</h2>`,
})
export class FitTitle {
  readonly text = input.required<string>();
  private readonly title = viewChild.required<ElementRef<HTMLElement>>('title');

  constructor() {
    // Einmal, nach dem ersten Rendern, NUR im Browser (auf dem Server ein No-op).
    afterNextRender({
      // 1. earlyRead: das Layout lesen, bevor geschrieben wird
      earlyRead: () => {
        const el = this.title().nativeElement;
        return Math.min(1, el.clientWidth / el.scrollWidth); // 1 = passt
      },
      // 2. write: erhält das Ergebnis der vorherigen Phase und schreibt nur
      write: (ratio) => {
        this.title().nativeElement.style.fontSize = `${Math.floor(32 * ratio)}px`;
      },
    });
  }
}
LivePLATFORM_ID und der Zeitpunkt der Messung (echte API)
AusdruckHier (live) Auf dem Server wäre es
inject(PLATFORM_ID)'server''server'
isPlatformBrowser(id)falsefalse
isPlatformServer(id)truetrue
inject(REQUEST)nullRequest (url, headers, cookie)
inject(RESPONSE_INIT)null{ status, headers }
Die gemessene KomponenteBreite jetzt (afterEveryRender, Phase read): 0 px
  1. +0.2 msconstructorisConnected=false, Breite=0px, Kinder=0, Binding=null
  2. +0.5 msngOnInitisConnected=true, Breite=0px, Kinder=0, Binding=""

Auf dem Server würden nur die Zeilen constructor und ngOnInit laufen (ohne Layout: die Breiten gäbe es nicht); afterNextRender und afterEveryRender werden ignoriert.

Die Tabelle zeigt die echten Werte dieser Seite. Drücke „Komponente neu mounten“ und lies das Log: Im Konstruktor ist das Element nicht auf der Seite und hat keinen Inhalt; in ngOnInit ist es eingehängt, aber die Template-Bindings sind noch nicht angewendet (und auf dem Server hätte es kein Layout); erst in den afterNextRender-Phasen ist alles gerendert. Bewege den Slider: afterEveryRender misst nach jedem Rendern neu.

afterEveryRender oder afterRenderEffect

afterEveryRender läuft nach jedem Rendern der Anwendung, auch wenn sich etwas in einer ganz anderen Komponente geändert hat. Hängt deine Arbeit von bestimmten Signals ab, ist afterRenderEffect die günstigere Option: ein Effect, der nach dem Rendern nur läuft, wenn sich seine Abhängigkeiten ändern (und wieder nur im Browser). Der Unterschied zu effect(): effect läuft auch auf dem Server und hat keine Lese-/Schreib-Phasen.

import { Component, ElementRef, afterRenderEffect, input, viewChild } from '@angular/core';

@Component({
  selector: 'app-chat-log',
  template: `
    <div #scroller class="messages">
      @for (m of messages(); track m.id) { <p>{{ m.text }}</p> }
    </div>
  `,
})
export class ChatLog {
  readonly messages = input.required<readonly { id: number; text: string }[]>();
  private readonly scroller = viewChild.required<ElementRef<HTMLElement>>('scroller');

  constructor() {
    // ✗ würde nach JEDEM Rendern der Anwendung laufen, auch wenn sich die Liste nicht geändert hat:
    // afterEveryRender({ write: () => this.scrollToBottom() });

    // ✓ läuft nach dem Rendern nur, wenn sich die gelesenen Signals geändert haben (weiterhin nur im Browser)
    afterRenderEffect({
      write: () => {
        this.messages(); // Abhängigkeit
        this.scrollToBottom();
      },
    });
  }

  private scrollToBottom(): void {
    const el = this.scroller().nativeElement;
    el.scrollTop = el.scrollHeight;
  }
}
Hydration ist nicht garantiert
Die API-Dokumentation ist eindeutig: Es ist nicht garantiert, dass Komponenten hydriert sind, wenn ein afterNextRender-Callback läuft. Lies und schreibe das DOM vorsichtig und verschiebe keine Knoten, die Angular rendert — sonst treten die Mismatch-Fehler auf, die in Hydration beschrieben sind.

Bibliotheken, die window berühren

Viele Bibliotheken (Karten, Diagramme, Rich-Text-Editoren) greifen schon beim Import auf window zu. Der Server stürzt ab, wenn er das Modul lädt, noch bevor eine Komponente läuft, und keine Prüfung im Konstruktor rettet dich mehr. Die Lösung ist ein dynamischer import() in afterNextRender; als Bonus landet die Bibliothek in einem eigenen Chunk. Für ganze Komponenten macht @defer dasselbe deklarativ.

import { Component, DestroyRef, ElementRef, afterNextRender, inject, viewChild } from '@angular/core';
// ✗ import * as L from 'leaflet';  — das Modul berührt window schon beim Import:
//    der Server stürzt beim Laden der Datei ab, noch bevor eine Komponente läuft.

@Component({
  selector: 'app-store-map',
  template: `<div #host class="map" aria-label="Karte der Geschäfte"></div>`,
  // eine feste Höhe reserviert den Platz auch im Server-HTML → kein Layout Shift
  styles: `.map { height: 320px; }`,
})
export class StoreMap {
  private readonly host = viewChild.required<ElementRef<HTMLElement>>('host');
  private readonly destroyRef = inject(DestroyRef); // inject() nur synchron, nicht nach await

  constructor() {
    afterNextRender(async () => {
      const L = await import('leaflet'); // eigener Chunk, nur im Browser geladen
      if (this.destroyRef.destroyed) return; // der Nutzer ist inzwischen weg
      const map = L.map(this.host().nativeElement).setView([44.4268, 26.1025], 12);
      this.destroyRef.onDestroy(() => map.remove());
    });
  }
}

Schreibt die Bibliothek das von Angular gerenderte DOM um, meldet die Hydration Mismatches. ngSkipHydration an der Host-Komponente stoppt die Hydration nur für diese — das ist ein Pflaster, keine Lösung.

DOCUMENT, PLATFORM_ID und Platform-Provider

inject(DOCUMENT) — seit v20 aus @angular/core; @angular/common exportiert es erneut — liefert dir auf jeder Plattform das aktuelle Dokument. Auf dem Server ist es das Domino-Dokument des Requests: Du kannst die Struktur lesen und schreiben (title, <meta>, <link rel="canonical">, Attribute auf <html>), und die Änderungen landen im gesendeten HTML. Layout, Cookies oder matchMedia gibt es nicht.

import { DOCUMENT, Service, inject } from '@angular/core';

@Service()
export class CanonicalLink {
  private readonly document = inject(DOCUMENT); // auf dem Server: das Domino-Dokument des Requests

  /** Läuft auch auf dem Server: Das Tag landet im HTML, das Crawler sehen. */
  set(url: string): void {
    let link = this.document.head.querySelector<HTMLLinkElement>('link[rel="canonical"]');
    if (!link) {
      link = this.document.createElement('link');
      link.rel = 'canonical';
      this.document.head.appendChild(link);
    }
    link.href = url;
  }
}

// Für <title> und <meta> gibt es in @angular/platform-browser bereits SSR-sichere Services:
//   inject(Title).setTitle('Phone X — Store');
//   inject(Meta).updateTag({ name: 'description', content: '…' });

inject(PLATFORM_ID) liefert 'browser' oder 'server', und du prüfst es mit isPlatformBrowser / isPlatformServer aus @angular/common. Das funktioniert, aber der offizielle Leitfaden empfiehlt Platform-Provider statt Laufzeitprüfungen, aus zwei Gründen: Die Prüfungen vervielfachen sich im ganzen Code, und in einem @if im Template eingesetzt erzeugen sie auf dem Server und im Browser unterschiedliche Inhalte, also Layout Shift und Hydration-Fehler. Heb es dir für Seiteneffekte auf, die nicht ändern, was gerendert wird.

import { isPlatformBrowser } from '@angular/common';
import { PLATFORM_ID, Service, inject } from '@angular/core';

@Service()
export class Analytics {
  // PLATFORM_ID ist 'browser' oder 'server'; vergleiche es nicht von Hand, nimm die Helfer aus @angular/common
  private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

  track(event: string): void {
    if (!this.isBrowser) return; // ok: ein Seiteneffekt, der nicht ändert, was gerendert wird
    navigator.sendBeacon('/collect', JSON.stringify({ event, at: Date.now() }));
  }
}

// ✗ NICHT so — Server und Browser rendern unterschiedliche Inhalte → Layout Shift und Hydration-Fehler:
//   @if (isBrowser) { <app-recommendations /> } @else { <p>Lädt…</p> }
// ✓ Nimm @defer (on viewport) oder afterNextRender für das, was nur im Browser Sinn ergibt.

Brauchen die Plattformen unterschiedliches Verhalten, ist der saubere Weg ein Vertrag mit zwei Implementierungen: Die Komponenten injizieren die abstrakte Klasse, app.config.ts stellt die Browser-Implementierung bereit, und app.config.server.ts überschreibt sie. Kein if im Geschäftscode — dasselbe Prinzip wie in Providers und Injection Tokens.

import { Injectable } from '@angular/core';

// Der Vertrag: Komponenten hängen nur von der abstrakten Klasse ab.
export abstract class AnalyticsService {
  abstract trackEvent(name: string): void;
}

@Injectable()
export class BrowserAnalyticsService implements AnalyticsService {
  trackEvent(name: string): void {
    navigator.sendBeacon('/collect', name); // Browser-API — hier erlaubt
  }
}

@Injectable()
export class ServerAnalyticsService implements AnalyticsService {
  trackEvent(_name: string): void {
    // auf dem Server: nichts (oder ein Log), keine Browser-APIs
  }
}

TransferState

Ohne besondere Maßnahmen lädt eine SSR-Seite ihre Daten zweimal: einmal auf dem Server, um das HTML zu rendern, und noch einmal im Browser, weil die Komponenten bei der Hydration bei null anfangen, mit leerem Zustand. Das Ergebnis: ein zusätzlicher Request und der Inhalt vom Server, der kurz durch ein Skeleton ersetzt wird.

TransferState ist ein Schlüssel-Wert-Dictionary, das im HTML mitreist. Auf dem Server schreibst du hinein; vor der Serialisierung ruft Angular toJson() auf und legt das Ergebnis in ein <script id="ng-state" type="application/json"> am Ende von body — nur, wenn es nicht leer ist. Im Browser liest TransferState beim ersten Injizieren das Script wieder ein. Die Schlüssel entstehen mit makeStateKey<T>('…'), was zugleich den Typ des Werts bindet. Die vollständige API: get(key, default), set, remove, hasKey, isEmpty, onSerialize, toJson.

import { isPlatformServer } from '@angular/common';
import { PLATFORM_ID, Service, TransferState, inject, makeStateKey } from '@angular/core';

export interface Article { slug: string; title: string; body: string }

// Typisierter Schlüssel: TransferState weiß, dass darunter ein Article liegt
const articleKey = (slug: string) => makeStateKey<Article>(`article:${slug}`);

@Service()
export class ArticleStore {
  private readonly cms = inject(CmsClient); // SDK mit eigenem fetch → läuft NICHT über HttpClient
  private readonly state = inject(TransferState);
  private readonly isServer = isPlatformServer(inject(PLATFORM_ID));

  async load(slug: string): Promise<Article> {
    const key = articleKey(slug);
    const cached = this.state.get(key, null);
    if (cached) {
      this.state.remove(key); // nur einmal verwendet: spätere Navigationen holen frische Daten
      return cached;
    }
    const article = await this.cms.getArticle(slug);
    if (this.isServer) this.state.set(key, article); // landet in <script id="ng-state">
    return article;
  }
}
LiveTransferState: ein Request statt zwei (echte API)

Vom Server gesendetes HTML

Noch nichts. Starte das Rendern auf dem Server.

Was der Nutzer sieht

Die Seite ist noch nicht geladen.
0Requests an /api/products/3
Schritt 1 macht ein echtes GET (über das simulierte Backend) und schreibt in den TransferState; das JSON im Script ist genau das Ergebnis von toJson(). Schritt 2 erzeugt einen neuen TransferState aus einem Dokument, das dieses Script enthält, genau wie der Browser beim Bootstrap. Entferne den Haken bei „TransferState verwenden“ und wiederhole: Der zweite Request und das Skeleton erscheinen.

Die Werte laufen durch JSON.stringify / JSON.parse: Ein Date wird zum String, eine Map und ein Set werden zu {}, Klasseninstanzen verlieren ihre Methoden. Übertrage einfache DTOs (siehe Interfaces & Models). Alles, was du dort ablegst, ist im Seitenquelltext sichtbar und macht das HTML schwerer: Die Demo zeigt, wie viele Zeichen es hinzufügt.

Für HttpClient schreibst du das nicht
GETs über HttpClient werden automatisch übertragen (nächster Abschnitt). Der manuelle TransferState ist für Daten, die nicht über HttpClient laufen: SDKs (Firebase, ein CMS), ein direktes fetch, teure Berechnungen, die einmal auf dem Server erledigt werden. Für Resources gibt es ab v22 außerdem resource({ id }).

Der HTTP-Transfer-Cache

provideClientHydration() startet standardmäßig einen Interceptor, der genau das obige Muster auf HttpClient anwendet. Auf dem Server speichert er die Responses im TransferState, unter einem Schlüssel, der als SHA-256-Hash aus Methode, Response-Typ, URL, Body und Parametern berechnet wird. Im Browser liefert er sie aus dem Cache aus — aber nur, bis die Anwendung zum ersten Mal stabil wird; danach geht alles wie gewohnt ans Netzwerk. httpResource läuft über HttpClient, profitiert also automatisch, ebenso deine Interceptors (siehe HTTP).

In 22.2.2 werden standardmäßig nur GET- und HEAD-Requests gecacht, die diese Bedingungen erfüllen:

  • sie haben keine Header Authorization, Proxy-Authorization oder Cookie;
  • sie werden nicht mit withCredentials oder mit credentials: 'include' / 'same-origin' gesendet;
  • sie haben kein Cache-Control mit no-store, no-cache oder private (weder im Request noch in der Response) und verwenden nicht cache: 'no-store' / 'no-cache';
  • ihre Response enthält kein Set-Cookie.
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration, withHttpTransferCacheOptions } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    // Der Transfer-Cache ist mit provideClientHydration() standardmäßig AN;
    // withHttpTransferCacheOptions justiert ihn nur.
    provideClientHydration(
      withHttpTransferCacheOptions({
        // welche Response-Header gespeichert werden (Standard: keine)
        includeHeaders: ['ETag'],
        // false → der Request wird nicht gecacht (nutzerbezogene oder oft wechselnde Daten)
        filter: (req) => !req.url.includes('/api/cart'),
        // lesende POSTs (z. B. GraphQL) — nur wenn sie idempotent sind
        includePostRequests: true,
        // Standard false: Requests mit Authorization / Proxy-Authorization / Cookie werden übersprungen
        includeRequestsWithAuthHeaders: false,
      }),
    ),
  ],
};
OptionStandardWann ändern
filter(req)alle infrage kommenden Requestsdu nimmst nutzerbezogene oder sehr flüchtige Endpoints heraus (false = nicht gecacht)
includeHeaderskein Headerder Browser braucht ETag, Link… Niemals sensible Header
includePostRequestsfalseein POST für Lesezugriffe (GraphQL), nur wenn er idempotent ist
includeRequestsWithAuthHeadersfalseder Auth-Header ändert die Response nicht (ein öffentlicher Schlüssel)
includeRequestsWithCredentialsfalseResponses mit Credentials sind garantiert öffentlich
includeNonCacheableRequestsfalsedu ignorierst Cache-Control / Set-Cookie bewusst (selten)

Resources, die HttpClient nicht verwenden v22

Ab v22 akzeptieren resource() und rxResource() eine id. Der auf dem Server aufgelöste Wert wird unter dieser id in den TransferState geschrieben, und im Browser startet die Resource direkt im Zustand resolved, ohne den Loader erneut aufzurufen — wieder nur, solange der Hydration-Cache aktiv ist, also bis zur ersten Stabilität, und nur mit provideClientHydration() in der Konfiguration.

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

@Component({ selector: 'app-weather', template: `{{ forecast.value()?.summary }}` })
export class Weather {
  private readonly sdk = inject(WeatherSdk); // nicht HttpClient → der HTTP-Cache sieht es nicht

  // v22: Mit einer id wird der auf dem Server aufgelöste Wert in den TransferState gelegt und bei der Hydration wiederverwendet.
  // Die id muss auf der Seite eindeutig und auf dem Server und im Browser identisch sein.
  protected readonly forecast = resource({
    id: 'weather-bucharest',
    loader: () => this.sdk.forecast('Bucharest'),
  });
}

Unterschiedliche Origins auf dem Server und im Browser

Spricht der Server die API unter einer internen Adresse an und der Browser unter der öffentlichen, unterscheiden sich die URLs, und die Cache-Schlüssel passen nicht mehr zusammen. HTTP_TRANSFER_CACHE_ORIGIN_MAP (aus @angular/common/http) sagt dem Server, er soll den Schlüssel so berechnen, als hätte er die öffentliche Origin verwendet. Er wird ausschließlich in der Server-Konfiguration deklariert: Findet Angular ihn im Browser, wirft es den Fehler NG02803.

// api-base-url.ts — im Browser: die öffentliche Origin
export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL', {
  factory: () => 'https://api.magazin.ro',
});

// app.config.server.ts (Fragment) — auf dem Server: das interne Netz, ohne Umweg über den Load Balancer
providers: [
  provideServerRendering(withRoutes(serverRoutes)),
  { provide: API_BASE_URL, useValue: 'http://catalog.internal:8080' },
  // dieselben Pfade auf beiden Origins → die Schlüssel im Transfer-Cache stimmen überein
  // (nur in der Server-Konfiguration; im Browser wirft Angular einen Fehler, wenn es ihn findet)
  {
    provide: HTTP_TRANSFER_CACHE_ORIGIN_MAP,
    useValue: { 'http://catalog.internal:8080': 'https://api.magazin.ro' },
  },
];

Noch ein Detail ab v22, wenn HttpClient standardmäßig das fetch-Backend verwendet: Auf dem Server ist jeder Response-Body auf 1 MB begrenzt (sonst der Fehler NG02825). Das Limit änderst du mit provideServerRendering({ maxResponseBodySize }, withRoutes(…)), aber der Leitfaden empfiehlt, es klein zu halten: Große Downloads gehören nicht ins Server-Rendering.

Stabilität und PendingTasks

Der Server weiß nicht, wann deine Seite „fertig“ ist. Seine Regel ist einfach: Nach dem Bootstrap wartet er auf ApplicationRef.whenStable() und serialisiert erst dann das DOM und den TransferState. Die Anwendung ist stabil, wenn keine Tasks mehr in PendingTasks sind. Angular fügt selbst Tasks hinzu für laufende HttpClient-Requests, für die Router-Navigation und für ladende Resources (resource, httpResource). Mit Zone.js hält außerdem jeder Timer oder Microtask in der Angular-Zone die Anwendung instabil.

Zoneless-Anwendungen — seit v21 der Standard — sehen deine Timer und Promises nicht mehr. Lädst du Daten mit einem eigenen SDK, mit fetch oder nach einem setTimeout, serialisiert der Server bevor die Daten ankommen: Das HTML geht leer raus, und der Browser fragt sie erneut an. Mach sie mit PendingTasks sichtbar (stabil seit v20; die Methode run und der Operator pendingUntilEvent sind noch Developer Preview):

import { Component, PendingTasks, inject, signal } from '@angular/core';
import { pendingUntilEvent, toSignal } from '@angular/core/rxjs-interop';

@Component({ selector: 'app-price', template: `{{ price() }} · {{ stock() }}` })
export class Price {
  private readonly tasks = inject(PendingTasks);
  private readonly sdk = inject(PricingSdk); // eigene Promises, nicht HttpClient
  readonly price = signal<string | null>(null);

  // 1. run(): Die Stabilität wartet, bis die async-Funktion fertig ist (Developer Preview)
  constructor() {
    this.tasks.run(async () => this.price.set(await this.sdk.priceOf(3)));
  }

  // 2. add(): manuelle Kontrolle — das Aufräumen NICHT vergessen, sonst hört der Server nie auf zu warten
  async refresh(): Promise<void> {
    const done = this.tasks.add();
    try {
      this.price.set(await this.sdk.priceOf(3));
    } finally {
      done();
    }
  }

  // 3. pendingUntilEvent(): instabil bis zur ersten Emission / complete / error (Developer Preview)
  readonly stock = toSignal(this.sdk.stock$(3).pipe(pendingUntilEvent()), { initialValue: 0 });
}
LiveWann serialisiert der Server? (echtes whenStable, im Browser)
appRef.isStable: true
Was der Server senden würde (Snapshot bei whenStable()):
<p class="price"></p>
Beide Buttons laden einen Preis in 1,2 Sekunden und warten dann auf appRef.whenStable() — genau das, was der Server vor der Serialisierung tut. Ohne PendingTasks löst whenStable sofort auf und das „HTML“ geht leer raus; mit PendingTasks.run() wartet es auf die Daten. Beobachte auch die isStable-Pill.
Ein Task, der nie endet = ein Request, der nie endet
Server-Rendering hat kein Timeout: Wird ein Task in PendingTasks nie entfernt (ein add() ohne finally, ein Observable, das mit pendingUntilEvent nie etwas emittiert), hängt die Response, bis der Proxy sie abbricht. Mit Zone.js bewirkt ein bei der Initialisierung gestartetes setInterval dasselbe. Polling, Websockets und wiederkehrende Timer haben auf dem Server nichts verloren: Starte sie in afterNextRender.
import { provideStabilityDebugging } from '@angular/core';

// Im Dev-Modus standardmäßig aktiv, wenn du provideClientHydration() verwendest.
// Manuell: für SSR ohne Hydration oder im Produktions-Build, vorübergehend.
bootstrapApplication(App, {
  providers: [provideStabilityDebugging()],
});
// Wird die Anwendung nicht innerhalb von ~9 s stabil, loggt sie in die Konsole die PendingTasks,
// die sie beschäftigt halten. Mit Zone.js zusätzlich 'zone.js/plugins/task-tracking' für Stack Traces importieren.

Request, Response und der Status 404

@angular/core exportiert drei Tokens für den aktuellen Request. @angular/ssr füllt sie nur beim Rendern auf Anfrage (RenderMode.Server); im Browser, beim Prerendering, beim Build und beim Extrahieren der Routen im Dev-Modus sind sie null — verwende also immer ?..

TokenEnthältVerwendet für
REQUESTden Request der Web API: url, headersCookies, Accept-Language, eine absolute URL für Canonical / Open Graph
RESPONSE_INITein veränderbares ResponseInit: status, statusText, headersdynamisches 404 / 410, beim Rendern berechnete Header
REQUEST_CONTEXTunknown: das zweite Argument von handle(request, context)Tenant, Request-ID, der bereits von einer Middleware authentifizierte Nutzer
import { Component, RESPONSE_INIT, inject } from '@angular/core';

@Component({
  selector: 'app-product-missing',
  template: `<h1>Dieses Produkt gibt es nicht mehr</h1><a href="/produse">Zum Katalog</a>`,
})
export class ProductMissing {
  constructor() {
    // null im Browser, beim Prerendering (SSG) und zur Build-Zeit — deshalb das ?.
    const response = inject(RESPONSE_INIT);
    if (response) response.status = 404; // sonst wird ein „Soft 404“ als echte Seite indexiert
  }
}

Warum der Status wichtig ist: Eine Seite „Produkt existiert nicht“, die mit 200 ausgeliefert wird, ist ein Soft 404, den Suchmaschinen als echte Seite indexieren können. Der status der ServerRoute deckt die statischen Fälle ab (die Route **); für „die id existiert nicht in der Datenbank“ brauchst du RESPONSE_INIT. Das funktioniert, weil @angular/ssr die Response erst nach whenStable() baut, du den Status bis dahin also ändern kannst.

import { isPlatformBrowser } from '@angular/common';
import { PLATFORM_ID, REQUEST, Service, inject } from '@angular/core';

@Service()
export class RequestInfo {
  private readonly request = inject(REQUEST); // Request der Web API; null im Browser / bei SSG
  private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

  /** Bevorzugte Sprache: der Header auf dem Server, navigator im Browser. */
  preferredLanguage(): string {
    if (this.request) return this.request.headers.get('accept-language')?.split(',')[0].trim() || 'de';
    // kein Request: Browser ODER Prerendering. NICHT "typeof navigator" prüfen — Node 21+ hat es.
    return this.isBrowser ? navigator.language : 'de';
  }

  cookie(name: string): string | null {
    const raw = this.request?.headers.get('cookie') ?? '';
    const match = raw.split('; ').find((c) => c.startsWith(`${name}=`));
    return match ? decodeURIComponent(match.slice(name.length + 1)) : null;
  }
}

Relative und absolute URLs

Auf dem Server gibt es keine „aktuelle Seite“, gegen die sich eine relative URL auflösen ließe. HttpClient löst das Problem selbst: @angular/platform-server fügt einen Interceptor hinzu, der /api/… anhand der Request-URL und von <base href> in eine absolute URL umwandelt. Ein direktes fetch() und SDKs, die es nutzen, profitieren davon nicht.

// ✓ HttpClient auf dem Server: Die relative URL wird gegen die URL des aktuellen Requests aufgelöst
this.http.get('/api/products');          // → https://magazin.ro/api/products

// ✗ fetch() direkt (oder ein SDK, das es nutzt) — Node hat keine „aktuelle Seite“:
await fetch('/api/products');            // TypeError: Failed to parse URL from /api/products

// ✓ Wenn du fetch() verwenden musst: Baue die absolute URL aus dem Request
const base = inject(REQUEST)?.url ?? inject(DOCUMENT).location.href;
await fetch(new URL('/api/products', base));

Gerade weil die URLs aus dem Host des Requests gebaut werden, prüft AngularNodeAppEngine den Hostnamen gegen allowedHosts (in angular.json unter build.options.security.allowedHosts oder über die Variable NG_ALLOWED_HOSTS) und antwortet bei einem unbekannten Host mit 400. Das ist der Schutz gegen SSRF: Konfiguriere es mit deinen Domains.

Zustand, der zwischen Requests durchsickert

Im Browser wird ein JavaScript-Modul einmal pro Tab ausgewertet, also pro Nutzer. Auf dem Server wird dasselbe Modul einmal pro Prozess ausgewertet und bedient Tausende Requests. Jede Variable auf Modulebene wird zu Zustand, den alle Nutzer teilen. Ein Provider mit useValue hat dasselbe Problem: Das Objekt wird beim Laden des Moduls erzeugt und lebt, bis der Server neu startet — der SSR-Leitfaden sagt das ausdrücklich und empfiehlt useFactory.

Services sind sicher: Bei jedem Request erzeugt @angular/ssr eine neue Plattform und eine neue Anwendung, also einen neuen Root-Injector. Jeder Nutzer bekommt seine eigenen Instanzen, die nach der Response zerstört werden.

// ✗ Zustand auf Modulebene: Die Datei wird EINMAL pro Node-Prozess ausgewertet,
//    also teilen ALLE Requests (und alle Nutzer) die Variablen.
let currentUser: User | null = null;          // Nutzer B sieht Nutzer A
const cart = new Map<number, number>();       // der Warenkorb wächst mit jedem Request

// ✗ Dasselbe mit useValue: Das Objekt wird einmal erzeugt, beim Laden des Moduls
{ provide: CART_STATE, useValue: { items: [] } }

// ✓ Ein Service: Auf dem Server startet jeder Request eine neue Anwendung,
//    also bekommt er eine neue Instanz (die nach der Response zerstört wird).
@Service()
export class CartStore {
  private readonly items = signal<CartItem[]>([]);
  readonly count = computed(() => this.items().length);
}

// ✓ Oder useFactory: läuft für jede erzeugte Anwendung
{ provide: CART_STATE, useFactory: () => ({ items: [] }) }
Der „harmlose“ Cache neben einem Service
const cache = new Map() auf Modulebene sieht nach einer Optimierung aus. Auf dem Server ist es ein Speicherleck (es wächst endlos) und, wenn der Schlüssel den Nutzer nicht enthält, ein Datenleck zwischen Konten. Willst du einen Cache zwischen Requests, mach ihn explizit — in server.ts oder am CDN — mit Schlüsseln, TTL und Größenlimit.

Fallstudie: das Theme

Das Theme ist das klassische Beispiel: Die Einstellung lebt in localStorage, und das CSS hängt von einem Attribut auf <html> ab. Diese Seite ist eine SPA ohne SSR, deshalb ist ihr ThemeStore hier korrekt. Aber es ist eine gute Übung: Wenn wir morgen mit SSR anfingen, was würde kaputtgehen?

// ThemeStore dieser Seite (Fragment). Die Seite ist eine SPA, also ist er HIER korrekt.
@Service()
export class ThemeStore {
  private readonly document = inject(DOCUMENT);
  // ✗ auf dem Server: defaultView ist das Domino-Window, das KEIN matchMedia hat → TypeError
  private readonly media = this.document.defaultView?.matchMedia('(prefers-color-scheme: dark)');

  // readStoredMode() liest localStorage in einem try/catch: Auf dem Server stürzt es nicht ab,
  //    liefert aber immer 'system' — der Server kann nicht wissen, was der Nutzer gewählt hat.
  readonly mode = signal<ThemeMode>(readStoredMode());

  constructor() {
    this.media?.addEventListener('change', (e) => this.systemDark.set(e.matches));
    effect(() => {
      // ✓ DOM über DOCUMENT: funktioniert auch auf dem Server
      // localStorage in try/catch: Auf dem Server wird es stillschweigend ignoriert
    });
  }
}

Zwei Probleme. Das erste: Die Zeile mit matchMedia wirft beim Erzeugen des Service, und ?. hilft nicht, weil auf dem Server defaultView existiert (das Fragment defaultView?.matchMedia(…) in der ersten Demo reproduziert genau das). Das zweite: Der Server rendert immer das Theme system, weil er localStorage nicht sehen kann. Die SSR-sichere Variante verlegt die Einstellung in ein Cookie, das der Server aus REQUEST liest, und der effect() schreibt das Attribut auch auf dem Server:

import { isPlatformBrowser } from '@angular/common';
import {
  DOCUMENT, DestroyRef, PLATFORM_ID, REQUEST, Service,
  afterNextRender, computed, effect, inject, signal,
} from '@angular/core';

export type ThemeMode = 'system' | 'light' | 'dark';
const COOKIE = 'atlas-theme';

@Service()
export class ThemeStore {
  private readonly document = inject(DOCUMENT);
  private readonly request = inject(REQUEST);
  private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

  // Der Server weiß nicht, ob das System dunkel ist: Bei 'system' entscheidet das CSS
  // (prefers-color-scheme), und das Signal wird im Browser korrigiert.
  private readonly systemDark = signal(false);
  readonly mode = signal<ThemeMode>(this.readCookie());
  readonly isDark = computed(
    () => this.mode() === 'dark' || (this.mode() === 'system' && this.systemDark()),
  );

  constructor() {
    const destroyRef = inject(DestroyRef);
    afterNextRender(() => {
      const media = matchMedia('(prefers-color-scheme: dark)'); // nur Browser
      const onChange = (e: MediaQueryListEvent) => this.systemDark.set(e.matches);
      this.systemDark.set(media.matches);
      media.addEventListener('change', onChange);
      destroyRef.onDestroy(() => media.removeEventListener('change', onChange));
    });

    effect(() => {
      const mode = this.mode();
      // läuft auch auf dem Server → <html data-theme="dark"> steht schon im HTML, kein Flackern
      const root = this.document.documentElement;
      if (mode === 'system') root.removeAttribute('data-theme');
      else root.setAttribute('data-theme', mode);
      // Cookie (nicht localStorage): Der Server erhält es beim nächsten Request
      if (this.isBrowser) {
        this.document.cookie = `${COOKIE}=${mode}; path=/; max-age=31536000; samesite=lax`;
      }
    });
  }

  private readCookie(): ThemeMode {
    const raw = this.request?.headers.get('cookie') // Server: der Header des Requests
      ?? (this.isBrowser ? this.document.cookie : ''); // Browser (auf dem Server wirft cookie)
    const value = raw
      .split('; ')
      .find((c) => c.startsWith(`${COOKIE}=`))
      ?.slice(COOKIE.length + 1);
    return value === 'light' || value === 'dark' ? value : 'system';
  }
}

Die Alternative ohne Cookie: Du behältst localStorage und ein Inline-Script in index.html, das data-theme vor dem ersten Paint setzt — genau das, was diese Seite macht. Das funktioniert auch mit SSR (der Server sendet HTML ohne das Attribut, das Script setzt es im Browser vor der Anwendung), aber der Server kennt das Theme trotzdem nicht und kann daher zum Beispiel nicht die passenden Bilder oder theme-color wählen.

<!-- Die Alternative ohne Cookie (so macht es diese Seite): ein Inline-Script in <head>,
     das vor dem ersten Paint läuft, in jedem Render-Modus (CSR, SSR, Prerendering). -->
<script>
  try {
    var t = localStorage.getItem('atlas-theme');
    if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
  } catch (e) {}
</script>

Wann ja, wann nein

SSR-sicherer Code heißt nicht überall if (isBrowser), sondern dass jede Sache an der richtigen Stelle sitzt.

Verwenden
  • afterNextRender / afterRenderEffect für alles, was das DOM, das Layout, den Storage oder eine Browser-Bibliothek berührt.
  • inject(DOCUMENT), Title, Meta für alles, was vom Server aus im HTML stehen muss (SEO, Sharing).
  • Platform-Provider in app.config.server.ts, wenn Server und Browser Unterschiedliches tun.
  • Der Standard-HTTP-Cache für öffentliche GETs; ein manueller TransferState oder resource({ id }) für Daten, die nicht über HttpClient laufen.
  • PendingTasks oder resource() für jede asynchrone Arbeit, deren Ergebnis im HTML stehen muss.
  • RESPONSE_INIT für dynamische Status, REQUEST für Cookies und Sprache.
Vermeiden (und was stattdessen)
  • isPlatformBrowser in einem @if im Template → @defer oder afterNextRender, mit demselben HTML auf beiden Plattformen.
  • typeof window quer durch die Komponenten verstreut → ein Service mit separater Server-Implementierung.
  • TransferState für große Listen oder nutzerbezogene Daten auf am CDN gecachten Seiten → im Browser laden.
  • PendingTasks für Polling, Websockets, Analytics → in afterNextRender starten; der Server soll nicht darauf warten. Ein vollständiges Beispiel steht auf der Seite WebSockets.
  • HTTP-Cache für sensible Responses → filter oder transferCache: false.
  • ngSkipHydration als Dauerlösung → die direkte DOM-Manipulation beheben.

Häufige Stolperfallen

effect() läuft auch auf dem Server
Viele behandeln effect() als „Browser-Hook“. Das ist er nicht: Er läuft bei der Change Detection, also auch auf dem Server. Ein effect, der in localStorage schreibt, matchMedia aufruft oder einen Observer startet, bricht beim Rendern. Was dem Browser gehört, kommt in afterRenderEffect oder unter afterNextRender.
?. ist kein Plattform-Guard
document.defaultView?.matchMedia(…) und typeof navigator !== 'undefined' sehen sicher aus, aber auf dem Server existiert defaultView (das Domino-Window, ohne matchMedia), und Node 21+ hat ein navigator. Das erste wirft einen TypeError, das zweite liefert die falsche Antwort. Verwende afterNextRender oder isPlatformBrowser.
  • Messungen im Konstruktor oder in ngOnInit: Sie brechen auf dem Server (es gibt kein Layout). Im Browser misst der Konstruktor 0, und in ngOnInit sind die Bindings und die @if / @for-Blöcke der Komponente noch nicht gerendert. Lösung: afterNextRender({ read }).
  • Im Konstruktor gestartete Timer und Subscriptions: Mit Zone.js blockieren sie die Serialisierung, zoneless laufen sie einfach weiter. Lösung: in afterNextRender starten und mit DestroyRef stoppen.
  • pendingTasks.add() ohne finally: Eine Exception, und der Request endet nie. Lösung: try/finally oder run().
  • Nicht verfolgtes Async in zoneless: fetch, SDKs, setTimeout → leeres HTML + ein weiterer Request im Browser. Lösung: resource() oder PendingTasks.
  • Generische TransferState-Schlüssel ('data') oder eine statische resource({ id }) in einer Komponente, die zweimal auf der Seite verwendet wird: Die Werte überschreiben sich gegenseitig. Lösung: Schlüssel, die die Parameter enthalten (article:${slug}), eindeutige IDs pro Seite.
  • Der Schlüssel wird nach dem Lesen nicht entfernt: Eine spätere Navigation auf derselben Seite bekommt die alten Daten aus dem HTML. Lösung: state.remove(key) nach der ersten Verwendung.
  • HTTP_TRANSFER_CACHE_ORIGIN_MAP in app.config.ts: Fehler NG02803 im Browser. Lösung: nur in app.config.server.ts.
  • Ein relatives fetch('/api/…') auf dem Server: TypeError: Failed to parse URL. Lösung: HttpClient oder eine aus REQUEST gebaute absolute URL.
  • Eine „Nicht gefunden“-Seite mit Status 200: ein Soft 404 bei Google. Lösung: RESPONSE_INIT oder status: 404 auf der Route **.
  • Werte, die sich zwischen Server und Browser unterscheiden: Date.now(), Math.random(), Datumsangaben in der Zeitzone des Servers (oft UTC). Der Text ändert sich bei der Hydration. Lösung: einmal auf dem Server erzeugen und übertragen oder nur im Browser anzeigen.
  • Zustand auf Modulebene und useValue mit Objekten: werden von den Nutzern geteilt. Lösung: Services oder useFactory.

Spickzettel

Du willst…Verwende
das DOM / das Layout einmal nach dem Rendern berührenafterNextRender({ earlyRead, write, read })
das DOM mit bestimmten Signals synchronisierenafterRenderEffect(() => …)
nach jedem Rendern messenafterEveryRender({ read })
eine Bibliothek laden, die window berührtawait import('lib') in afterNextRender, oder @defer
title, Meta, Canonical lesen / schreibeninject(DOCUMENT), Title, Meta
eine andere Implementierung auf dem Servereine abstrakte Klasse + ein Provider in app.config.server.ts (mergeApplicationConfig)
ein Seiteneffekt nur im BrowserisPlatformBrowser(inject(PLATFORM_ID))
die GETs im Browser nicht wiederholenprovideClientHydration() (Standard), withHttpTransferCacheOptions(…)
einen Request aus dem Cache nehmenfilter oder { transferCache: false }
Daten übertragen, die nicht über HttpClient kommenmakeStateKey<T>() + TransferState, oder resource({ id }) (v22)
unterschiedliche API-Origins auf dem Server und im BrowserHTTP_TRANSFER_CACHE_ORIGIN_MAP, nur auf dem Server
dass der Server auf Asynchrones wartetresource(), PendingTasks.run() / add(), pendingUntilEvent()
herausfinden, warum die Anwendung nicht stabil wirdprovideStabilityDebugging()
Cookies / Header leseninject(REQUEST)?.headers.get('cookie')
mit einem 404 antworteninject(RESPONSE_INIT) → status = 404, oder status: 404 auf der Route **
Kontext aus server.ts erhaltenhandle(req, ctx) → inject(REQUEST_CONTEXT)
nutzerbezogener Zustand auf dem ServerServices oder useFactory — niemals Modulvariablen

Kap. 12 · 0 von 3 abgeschlossen

Lektion abgeschlossen?

Markiere sie, dann erscheint sie auf deiner Karte: im Inhaltsverzeichnis und auf der Startseite.

Wird automatisch markiert, sobald du die ganze Lektion gelesen hast.

  1. SSR, Prerendering & Render Modes
  2. Hydration: Event Replay & inkrementell
  3. Servertauglicher Code
Deine ganze Karte ansehen

Alle 75 Seiten · tippen zum Filtern

  • 01Was ist neu in Angular 21 & 22Alle wichtigen Änderungen in v21 und v22 in Kürze, mit Link zur Seite, die sie jeweils erklärt.Start
  • 01Deine erste App, Schritt für SchrittEine Leseliste, von einer leeren Komponente bis zur fertigen App — Signals, @for, Inputs und Outputs, ein Service und ein Formular, Schritt für Schritt.Start
  • 01Schnelle Entscheidungen„Wohin damit?“, „Signal oder Observable?“, „Shared oder Feature?“ — kurze Antworten auf Alltagsfragen.Start
  • 01Angular-Interviewfragen60 echte Interviewfragen für Junior, Mid und Senior, mit der passenden Antwort, der Nachfrage und den Warnsignalen. Dazu ein Probeinterview.Start
  • 02Standalone & BootstrapWie eine App ohne NgModule startet: bootstrapApplication, app.config.ts, imports in der Komponente.Moderne Grundlagen
  • 02Data Binding & EventsInterpolation, Property- und Attribut-Bindings, Events mit $event und Tastenfiltern und wie [( )] aufgebaut ist — was in das DOM geht und was zurückkommt.Moderne Grundlagen
  • 02Control Flow: @if, @for, @switchDie Block-Syntax, die *ngIf/*ngFor/ngSwitch ersetzt, mit track, @empty und Exhaustive Checks.Moderne Grundlagen
  • 02@let und die neuen Template-AusdrückeLokale Variablen mit @let, Arrow Functions, Spread, Regex, instanceof, Kommentare in Tags.Moderne Grundlagen
  • 02@defer — Lazy Loading in TemplatesSchwere Komponenten nur laden, wenn sie gebraucht werden: Trigger, Prefetch, Placeholder, Loading, Error.Moderne Grundlagen
  • 02Class- & Style-BindingsWarum [class.x] und [style.x] ngClass und ngStyle abgelöst haben (mit automatischer Migration ab v21).Moderne Grundlagen
  • 02Komponenten-Styles & Kapselung:host, die drei Kapselungsmodi, Theming mit CSS-Variablen und warum ::ng-deep der letzte Ausweg ist.Moderne Grundlagen
  • 02Pipes: wann und wiePure vs. Impure Pipes, eigene Pipes und wie du zwischen Pipe, computed() und Methode wählst.Moderne Grundlagen
  • 03signal() und computed()Die reaktiven Grundbausteine: beschreibbarer Zustand, abgeleiteter Zustand, Gleichheit, untracked und Regeln zur Immutability.Signals & Reaktivität
  • 03effect() — wann ja, wann neinMit Signals synchronisierte Seiteneffekte: localStorage, Logging, imperative APIs. Und ihre Stolperfallen.Signals & Reaktivität
  • 03linkedSignal() — editierbarer abgeleiteter ZustandEin Signal, das sich zurücksetzt, wenn sich seine Quelle ändert, das du aber auch manuell setzen kannst.Signals & Reaktivität
  • 03resource, rxResource, httpResourceAsynchrone Daten als Signals: status, value, error, reload, automatischer Abbruch. Stabil seit v22.Signals & Reaktivität
  • 03debounced() — verzögerte SignalsDie „Debounce“-Variante eines Signals, ohne RxJS. Perfekt für die Suche.Signals & Reaktivität
  • 03Listen: suchen, filtern, sortieren, blätternEine Kette von computed-Signals, die nur neu berechnet, was sich geändert hat — mit den Filtern in der URL, damit ein Link dieselbe Ansicht öffnet.Signals & Reaktivität
  • 03Observables von null anWas ein Observable ist, subscribe und unsubscribe, die Operatoren, die du jede Woche brauchst, cold vs. hot — und wann ein Signal reicht.Signals & Reaktivität
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — und wann RxJS weiterhin die richtige Wahl bleibt.Signals & Reaktivität
  • 04input() und output()Inputs als Signals (required, transform, alias) und typisierte Outputs. Daten nach unten, Events nach oben.Komponenten: Kommunikation
  • 04model() — Two-Way-BindingBanana-in-a-Box [( )] mit einer einzigen API. Wann du es nutzt und wann input + output besser ist.Komponenten: Kommunikation
  • 04viewChild & contentChildQueries als Signals: viewChild, viewChildren, contentChild, contentChildren, die Option read.Komponenten: Kommunikation
  • 04Lifecycle & DestroyRefWelche Hooks in der Signals-Ära noch zählen, afterNextRender, DestroyRef und typisiertes ngOnChanges.Komponenten: Kommunikation
  • 04Content Projectionng-content mit select, mehrere Slots, Standardinhalt und die Falle der bedingten Projektion.Komponenten: Kommunikation
  • 04ng-template & ngTemplateOutletTemplates als Werte: Render Props in Angular, typisierter Kontext und Anpassen von Komponenten.Komponenten: Kommunikation
  • 05Smart vs. DumbContainer vs. Presentational: wer den Zustand hält, wer nur anzeigt und wie du die beiden sauber trennst.Komponenten-Design
  • 05Generische Komponenten <T>Eine korrekt typisierte DataTable<T> oder Select<T>, mit Templates für Zellen.Komponenten-Design
  • 05Dynamische KomponentenNgComponentOutlet, createComponent mit inputBinding/outputBinding und ein Dashboard, das aus einer Konfiguration gebaut wird.Komponenten-Design
  • 05Direktiven & Host-KompositionAttribut-Direktiven, die host: {}-Metadaten und die Directive Composition API (hostDirectives).Komponenten-Design
  • 05Eigene Formular-ControlsFormValueControl (Signal Forms) vs. ControlValueAccessor: eine Sternebewertung, in beiden Varianten.Komponenten-Design
  • 05Change DetectionOnPush ist ab v22 der Standard, zone.js entfällt ab v21. Was das Rendering auslöst und wie du dir nicht die Finger verbrennst.Komponenten-Design
  • 06inject() & @Service()Der neue @Service()-Decorator ab v22, inject() vs. Konstruktor und der Injection-Kontext.Dependency Injection
  • 06Provider & die Injector-HierarchieRoot vs. Route vs. Komponente, useClass/useValue/useFactory/useExisting und die Modifier optional/self/skipSelf.Dependency Injection
  • 06InjectionToken & provideX()Typisierte Konfiguration, provideFeature()-Funktionen, makeEnvironmentProviders und Initializer.Dependency Injection
  • 06injectAsync() — Lazy ServicesLade einen schweren Service erst, wenn du ihn brauchst, mit Prefetch im Leerlauf.Dependency Injection
  • 07Ordnerstrukturcore / shared / features, was in welchen Ordner gehört und wie ein Feature von innen aussieht.Architektur der Anwendung
  • 07Shared vs. FeatureWann du eine Komponente nach Shared verschiebst und (vor allem) wann NICHT. Eine konkrete Checkliste.Architektur der Anwendung
  • 07Interfaces & ModelleInline-Interface oder eigene Datei? DTO vs. Domänenmodell, type vs. interface, wohin damit.Architektur der Anwendung
  • 07State Management mit SignalsEinfache Signal-Stores, Fassaden, lokaler vs. globaler Zustand und wann du NgRx brauchst.Architektur der Anwendung
  • 07Styleguide & NamensgebungDie aktuellen offiziellen Konventionen: Dateinamen ohne Suffixe, protected, readonly, Reihenfolge der Member.Architektur der Anwendung
  • 07Barrels, Aliase & Grenzenindex.ts, Pfad-Aliase, die öffentliche API eines Features und Abhängigkeitsregeln, die sich automatisch prüfen lassen.Architektur der Anwendung
  • 07Fehler: @boundary & ErrorHandlerDer neue @boundary-Block ab v22.2, der globale ErrorHandler, HTTP-Fehler und Fehler aus resource.Architektur der Anwendung
  • 08Modernes Routing & Lazy LoadingloadComponent, Child Routes, Parameter als Inputs und Parameter-Vererbung (neuer Standard in v22).Routing
  • 08Guards & ResolvercanActivate, canMatch, canDeactivate und resolve als Funktionen, mit Redirects über UrlTree / RedirectCommand.Routing
  • 09Signal Forms — die GrundlagenFormulare auf Basis von Signals: form(), [formField], schemabasierte Validierung. Stabil seit v22.Formulare
  • 09Signal Forms — fortgeschrittenEigene und feldübergreifende Validierung, async/HTTP mit Debounce, dynamische Listen, disabled/hidden, wiederverwendbare Schemas.Formulare
  • 09Typisierte Reactive FormsTypisierte FormGroup, nonNullable, FormArray — und wie sie neben Signal Forms bestehen.Formulare
  • 10HttpClient, Interceptors & die API-SchichtHttpClient ohne Setup (v21), Fetch als Standard (v22), funktionale Interceptors und eine API-Schicht mit DTO-Mapping.HTTP & Daten
  • 11Performance — eine ChecklisteWas wirklich zählt: OnPush + Signals, track, @defer, Lazy Routes, Bilder, Bundles und Hydration.Qualität & Performance
  • 11Testen mit VitestVitest ist ab v21 der Standard-Runner. TestBed, Komponenten mit Signals, Services, HTTP und Harnesses.Qualität & Performance
  • 11DevTools: ein Blick in deine AppChrome DevTools und Angular DevTools Schritt für Schritt: was jedes Panel zeigt, mit Übungen, die auf dieser Website funktionieren.Qualität & Performance
  • 11Angular AriaHeadless, barrierefreie Komponenten, die du nach Belieben stylst: Tabs, Accordion, Menü, Listbox...Qualität & Performance
  • 12SSR, Prerendering & Render ModesWann auf dem Server gerendert wird, wann beim Build und wann nur im Browser: RenderMode pro Route, app.routes.server.ts und die echten Kosten.SSR & Hydration
  • 12Hydration: Event Replay & inkrementellWie der Browser das Server-HTML übernimmt, ohne es neu zu rendern: vollständige Hydration, Event Replay, @defer (hydrate on …) und Mismatch-Fehler.SSR & Hydration
  • 12Servertauglicher Codewindow gibt es auf dem Server nicht: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, der HTTP-Cache und PendingTasks.SSR & Hydration
  • 13Micro FrontendsWann es sich lohnt, eine App in mehrere Deployments aufzuteilen und wann es nur Komplexität ist: Native Federation, Module Federation, iframes, Web Components.Architektur im großen Maßstab
  • 13Web Components mit Angular ElementscreateCustomElement: eine Angular-Komponente, die überall als HTML-Tag nutzbar ist, plus Kommunikation zwischen Apps über Events und BroadcastChannel.Architektur im großen Maßstab
  • 13Monorepo, Bibliotheken & GrenzenEin Workspace mit mehreren Apps und Bibliotheken: ng generate library, Nx, Secondary Entry Points, Publishing und Abhängigkeitsregeln.Architektur im großen Maßstab
  • 13Hexagonale Architektur (Ports & Adapters)Eine Domäne ohne Angular, Ports als abstrakte Klassen und austauschbare Adapter: HTTP, localStorage oder In-Memory, ohne die UI anzufassen.Architektur im großen Maßstab
  • 14Signals unter der HaubeDer reaktive Graph: Producer und Consumer, Push-Pull, warum computed lazy und glitch-frei ist, das Diamond-Problem und wann Effects laufen.Fortgeschrittene Reaktivität
  • 14Fortgeschrittenes RxJS: Operatoren höherer OrdnungswitchMap, mergeMap, concatMap und exhaustMap an einem Live-Diagramm, dazu Retry mit Backoff, shareReplay und wie du Leaks vermeidest.Fortgeschrittene Reaktivität
  • 14NgRx SignalStoreState Management im großen Stil auf Signals: withState, withComputed, withMethods, rxMethod, Entities und Hooks — und wann ein einfacher Store reicht.Fortgeschrittene Reaktivität
  • 14Zustandsautomaten mit SignalsKeine unmöglichen Zustände mehr: ein typisierter Automat auf Signals, mit Übergängen, Guards und einem Live-Zustandsdiagramm.Fortgeschrittene Reaktivität
  • 15WebSockets: Echtzeit, die das Netz überstehtEin typisierter Socket als Signals: Reconnect mit Backoff und Jitter, Heartbeat, ein Postausgang im Offline-Modus und SSR-Sicherheit.Echtzeit & offline
  • 15Optimistische Updates & RollbackEine sofortige UI, die ehrlich bleibt: Snapshot und Rollback, Antworten in falscher Reihenfolge, Idempotenzschlüssel.Echtzeit & offline
  • 15Offline-first: Service Worker & CachingDer Angular Service Worker: Caching-Strategien, Update-Hinweise und ein Postausgang für Änderungen, die offline entstehen.Echtzeit & offline
  • 16Web WorkersSchwere Berechnungen, ohne die UI zu blockieren: ein echter Worker in der Seite, Message Passing und wann es sich nicht lohnt.Fortgeschrittene UI & Performance
  • 16Riesige Listen: Virtual Scroll100.000 Zeilen ohne Ruckeln: CDK Virtual Scroll, itemSize, trackBy und die Alternativen (Paginierung, Infinite Scroll).Fortgeschrittene UI & Performance
  • 16Moderne Animationen & View Transitionsanimate.enter und animate.leave statt des veralteten @angular/animations, CSS-Transitions und View Transitions bei der Navigation.Fortgeschrittene UI & Performance
  • 16Memory Leaks finden und behebenSubscriptions, Timer und Listener, die ihre Komponente überleben – wie du sie in den DevTools findest und schließt.Fortgeschrittene UI & Performance
  • 17Sicherheit: XSS, Sanitizing & CSPWas Angular automatisch gegen XSS tut, wann bypassSecurityTrust gefährlich wird, dazu CSP mit Nonce, Trusted Types und XSRF.Sicherheit & Globalisierung
  • 17Authentifizierung: Tokens & RefreshEin Interceptor, der das Token anhängt, bei 401 einmal erneuert und die wartenden Requests wiederholt — dazu, wo du das Token aufbewahrst.Sicherheit & Globalisierung
  • 17OAuth 2.1 & OpenID Connect mit PKCEDer Authorization-Code-Flow mit PKCE Schritt für Schritt: Verifier und Challenge, State und Nonce, das ID-Token und warum ein BFF sicherer ist.Sicherheit & Globalisierung
  • 17Internationalisierung (i18n)$localize, das i18n-Attribut, ICU plural/select, ein Build pro Sprache vs. Übersetzungen zur Laufzeit und Formatierung nach Locale.Sicherheit & Globalisierung