AngularAtlasAusg. 22.2

Kap. 12SSR & Hydration54 / 75

SSR, Prerendering & Render Modes

Wann auf dem Server gerendert wird, wann beim Build und wann nur im Browser: RenderMode pro Route, app.routes.server.ts und die echten Kosten.

v22Architektur ~0 Min. Lesezeit Offizielle Doku

Standardmäßig liefert Angular die Anwendung als CSR (Client-Side Rendering) aus: Der Server schickt ein leeres <app-root></app-root>, und der Browser muss erst das gesamte JavaScript herunterladen, parsen und ausführen — und danach auch noch die Daten laden —, bevor er etwas Brauchbares anzeigt. SSR (Rendering auf dem Server, bei jedem Request) und SSG (Prerendering, beim Build) verlagern das erste Rendering woandershin: Der Nutzer bekommt sofort HTML mit Inhalt.

Mit @angular/ssr entscheidest du nicht einmal für die ganze Anwendung, sondern pro Route: Die Datei app.routes.server.ts legt für jeden Pfad fest, ob er RenderMode.Prerender, RenderMode.Server oder RenderMode.Client ist. Das nennt man Hybrid Rendering. Die API für Server-Routen kam mit v19 und ist seit v20 stabil.

Kurz gesagt
  • Rendern im Browser (CSR), auf dem Server (SSR) oder beim Build (Prerender) wählst du pro Route, mit RenderMode in app.routes.server.ts.
  • Prerender für Seiten, die für alle gleich aussehen, SSR für personalisierte Seiten oder Daten, die sich oft ändern, CSR für Bereiche hinter dem Login.
  • SSR verbessert die erste Anzeige und das, was ein Crawler sieht, bringt aber einen Server zum Betreiben und Code, der auch auf dem Server laufen muss.
  • Routen mit Parametern lassen sich prerendern, wenn du ihnen die Liste der Werte gibst, mit getPrerenderParams.
  • Das Caching am CDN zählt oft mehr als die Geschwindigkeit des Servers.
Warum das wichtig ist

SSR ist kein kostenloser Performance-Schalter. Du gewinnst LCP, SEO und korrekte Vorschauen in sozialen Netzwerken; du zahlst mit einem Node-Server, den du betreiben musst, mit Code, der auch ohne window laufen muss, und mit einem Zeitfenster, in dem die Seite sichtbar ist, aber noch nicht auf Klicks reagiert. Die richtige Entscheidung triffst du pro Route, nicht pro Projekt.

Drei Rendering-Modi

CSR · ClientSSR · ServerSSG · Prerender
Das HTML entstehtim Browser, nach dem JSauf dem Server, bei jedem Requestbeim Build, einmalig
Der Server, pro Requestliefert eine statische Datei (die leere Shell)startet die Anwendung, holt die Daten, rendertliefert eine statische Datei
TTFBkleinenthält das Rendering und die dabei aufgerufenen APIsminimal, besonders von einem CDN
LCPnach JS + Bootstrap + API-Requestsobald das HTML ankommtsobald das HTML ankommt
SEO, Vorschauenschwach: hängt vom JS abausgezeichnetausgezeichnet
Inhalt pro Nutzerjajanein — für alle identisches HTML
Hostingjedes statische Hosting / CDNeine Node-Runtime (oder eine andere JS-Runtime)jedes statische Hosting / CDN
Kosten pro Request≈ 0CPU + Speicher für ein vollständiges Rendering≈ 0

Was der Server im Server-Modus konkret tut: Für jeden Request erzeugt @angular/ssr eine neue Plattform, ruft die Bootstrap-Funktion aus main.server.ts auf, lässt den Router zur URL navigieren, wartet, bis die Anwendung stabil ist (alle HTTP-Requests und PendingTasks abgeschlossen), serialisiert das DOM zu HTML und zerstört die Anwendung. Die praktische Folge: Der TTFB einer SSR-Seite ist mindestens so lang wie die langsamste API, die beim Rendering aufgerufen wird.

In einer echten Hybrid-Anwendung existieren die Modi nebeneinander: Die Startseite und die Marketing-Seiten werden prerendert, die Suche wird auf dem Server gerendert, und der Kontobereich läuft nur im Browser.

Was bei einem Request passiert

Fünf Momente zählen, in der Reihenfolge, in der der Nutzer sie erlebt:

  • TTFB (Time to First Byte) — das erste Byte der HTML-Antwort.
  • FCP (First Contentful Paint) — der erste Text oder das erste Bild auf dem Bildschirm.
  • LCP (Largest Contentful Paint) — das Hauptelement erscheint (der Titel, das Produktbild). Das ist die Metrik, die der Nutzer als „es ist geladen“ wahrnimmt.
  • Hydration — das JS ist angekommen, Angular übernimmt das vorhandene HTML und hängt die Listener an.
  • Interaktiv — ein Klick bewirkt tatsächlich etwas. Ab jetzt zählt INP (wie schnell die Seite reagiert).

Bei SSR und SSG gibt es zwischen LCP und „interaktiv“ ein Zeitfenster, in dem die Seite fertig aussieht, die Buttons aber noch keinen Listener haben. Auf einem durchschnittlichen Smartphone mit einem Bundle von einigen hundert KB dauert dieses Fenster leicht 1–3 Sekunden. Starte die Simulation und vergleiche die drei Modi im selben Netzwerk.

LiveDie Timeline eines Requests (Simulation)
magazin.ro/produse/42
Shop
Kabellose Kopfhörer Aria X2
349 RON · Lieferung morgen
interaktiv
0 ms500 ms1,00 s1,50 s2,00 s
Server: API + Rendering
HTML · 40 KB
main.js · 260 KB
Hydration
TTFB 260 msFCP = LCP 535 msinteraktiv 2,36 snicht interaktiv 1,82 s

SSR: Der TTFB enthält die Arbeit des Servers (der auf die API wartet), danach erscheint der Inhalt sofort. Gelbes Band = Inhalt sichtbar, aber ohne Listener bis zur Hydration.

Wähle den Modus und das Netzwerk, zieh dann den Cursor. Das gelbe Band ist das Fenster „sichtbar, aber nicht interaktiv“; klicke währenddessen auf „In den Warenkorb“. Die Zahlen sind illustrativ, keine Messwerte.

Beachte, was sich nicht ändert: main.js ist in allen drei Modi gleich groß. SSR verkleinert das JavaScript nicht, die Interaktivität kommt also etwa zur gleichen Zeit wie bei CSR — manchmal sogar etwas später, weil das HTML größer ist und auch die Hydration CPU kostet. Der eigentliche Gewinn ist der LCP. Um das nicht interaktive Fenster zu verkürzen: weniger initiales JS (Lazy Routes, @defer), Incremental Hydration und Event Replay, das die im Fenster gemachten Klicks nicht verliert.

Was ein Crawler sieht

Ein Crawler geht vom empfangenen HTML aus (das, was du mit Quelltext anzeigen siehst, nicht im Elements-Tab). Bei CSR ist dieses HTML für jede URL identisch. Googlebot kann JavaScript ausführen, aber in einer späteren Phase und mit Einschränkungen; Bots, die Vorschauen in Chats und sozialen Netzwerken erzeugen, lesen meist nur das initiale HTML. Ein Detail wird oft übersehen: der Statuscode. Ein statischer Server weiß nicht, dass /produse/9999 nicht existiert, und antwortet deshalb mit 200 — einem „Soft 404“.

LiveWas ein Crawler erhält: CSR vs. SSR vs. SSG (Simulation)
curl https://magazin.ro/produse/42
HTTP/1.1 200 OK<!doctype html><html lang="de"><head>  <title>Shop</title>  <link rel="stylesheet" href="styles-5INURTSO.css"></head><body>  <app-root></app-root>  <script src="main-HG7KQWZB.js" type="module"></script></body></html>
Vorschau in einem Chat (ein Bot, der nur das HTML liest)
kein Bild
magazin.ro
Shop
— keine Beschreibung —
  • ✓Korrekter HTTP-Status200
  • ✕seitenspezifischer <title>Shop
  • ✕meta description / og:*fehlt
  • ✕Sichtbarer Text im HTML0 Wörter
  • ✕Links <a href> zum Verfolgen0

Dasselbe HTML für jede URL: Der gesamte Inhalt erscheint erst, nachdem JavaScript ausgeführt wurde. Vorschau-Bots führen es nicht aus; Googlebot schon, aber in einer späteren Phase und mit Einschränkungen.

Wechsle den Modus und aktiviere „nicht existierendes Produkt“. Beobachte den Status, den Titel, die Meta-Tags und die von Angular hinzugefügten Markierungen (ngh, jsaction, ng-state).

Titel und Meta-Tags setzt du mit den Services Title und Meta aus @angular/platform-browser. Sie arbeiten über DOCUMENT und funktionieren deshalb auch auf dem Server: Was du während des Renderings setzt, landet im gesendeten HTML. Für Seiten ohne Daten reicht die title-Eigenschaft der Route (oder eine eigene TitleStrategy, wenn du ein einheitliches Format willst).

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>(); // aus der Route produse/:id (withComponentInputBinding)

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

  constructor() {
    // Läuft auch auf dem Server: Die Tags landen im HTML, das an die Crawler geht.
    effect(() => {
      if (!this.product.hasValue()) return;
      const p = this.product.value();
      this.title.setTitle(`${p.name} — Shop`);
      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 });
    });
  }
}
Ein zentraler Ort für SEO

Packe die Logik in einen Service SeoService.update({ title, description, image, canonical }) und rufe ihn von den Detailseiten aus auf. Für <link rel="canonical"> brauchst du DOCUMENT — Details auf der Seite Server-kompatibler Code.

Setup: was ng add generiert

# neues Projekt, mit SSR + Prerendering von Anfang an konfiguriert
ng new magazin --ssr

# bestehendes Projekt: @angular/ssr, express und die Server-Dateien hinzufügen
ng add @angular/ssr

# Produktions-Build → dist/magazin/browser + dist/magazin/server
ng build

# den Node-Server starten (Script, das ng add in package.json einträgt)
npm run serve:ssr:magazin        # = node dist/magazin/server/server.mjs

Der Befehl fügt die Abhängigkeiten (@angular/ssr, express), vier Dateien und einige Optionen in angular.json hinzu. Es lohnt sich zu wissen, was jede davon tut, denn du wirst sie anpassen:

  • server.ts — der Express-Server. Er liefert die statischen Dateien aus dist/…/browser aus und gibt alle übrigen Requests an AngularNodeAppEngine.handle() weiter, das eine Response (oder null, wenn die URL keine Angular-Route ist) zurückgibt. Der Export reqHandler wird vom CLI verwendet: ng serve rendert ebenfalls auf dem Server.
  • main.server.ts — die Bootstrap-Funktion, die der Server bei jedem Rendering aufruft.
  • app.config.server.ts — provideServerRendering(withRoutes(serverRoutes)), über mergeApplicationConfig mit der Browser-Konfiguration kombiniert.
  • app.routes.server.ts — der Render-Modus pro Route. Standardmäßig ist alles Prerender.
  • app.config.ts erhält provideClientHydration(), damit der Browser das HTML wiederverwendet, statt es neu aufzubauen.
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(); // eine Instanz für den ganzen Server

// 1. (optional) deine APIs: app.get('/api/{*splat}', (req, res) => { ... });

// 2. Statische Dateien (JS/CSS/Bilder haben einen Hash im Namen → Cache für ein Jahr)
app.use(express.static(browserDistFolder, { maxAge: '1y', index: false, redirect: false }));

// 3. Alles andere: Angular entscheidet — prerendertes HTML, SSR oder die CSR-Shell
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}`);
  });
}

// wird von ng serve / ng build verwendet (und von manchen Serverless-Plattformen)
export const reqHandler = createNodeRequestHandler(app);

Ein Detail in server.ts: express.static hat index: false, daher wird nicht einmal / direkt von der Festplatte ausgeliefert. Jede Seite läuft durch Angular, das den für die Route konfigurierten Render-Modus, die Header und den Status anwendet.

allowedHosts in Produktion (v22)

Zum Schutz vor SSRF validiert der Server den Host-Header. Ab v22 bekommt ein unbekannter Host 400 Bad Request (vorher fiel der Server auf CSR zurück). ng add setzt "allowedHosts": []: Trage deine Domains in angular.json ein (Wildcards wie *.magazin.ro werden akzeptiert), in die Optionen von AngularNodeAppEngine oder in die Umgebungsvariable NG_ALLOWED_HOSTS. Hinter einem Load Balancer werden die X-Forwarded-*-Header ignoriert, bis du sie mit trustProxyHeaders ausdrücklich erlaubst. Details unter Sicherheit.

Wenn du ein älteres Projekt hast, dessen server.ts CommonEngine verwendet: Diese API ist seit v22 deprecated, der Ersatz ist AngularNodeAppEngine (Node) oder AngularAppEngine (Runtimes auf Basis der Web-API Request/Response).

RenderMode pro Route

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

export const serverRoutes: ServerRoute[] = [
  // Für alle gleich, ändert sich nur beim Deployment → HTML wird beim Build erzeugt (SSG)
  { path: '', renderMode: RenderMode.Prerender },
  { path: 'despre', renderMode: RenderMode.Prerender },

  // Öffentlich, indexiert, Daten ändern sich oft → bei jedem Request gerendert (SSR)
  { path: 'cautare', renderMode: RenderMode.Server },

  // Hinter dem Login: kein SEO, persönliches HTML → nur im Browser (CSR)
  { path: 'cont/**', renderMode: RenderMode.Client },

  // Das Sicherheitsnetz für jede Angular-Route, die oben nicht aufgeführt ist
  { path: '**', renderMode: RenderMode.Server },
];

Die Regeln, die in der Praxis zählen:

  • Matching: Ein exaktes Segment schlägt einen Parameter (:id), der wiederum ** schlägt. ** ist also das Sicherheitsnetz, keine Regel, die „überschreibt“.
  • Validierung in beide Richtungen: Jede Route im Router muss zu einer Server-Route passen, und jede Server-Route (außer denen, die auf /** enden) muss im Router existieren. Sonst bricht der Build mit einer eindeutigen Meldung ab.
  • Der Pfad beginnt nicht mit „/“ — '/despre' ist ein Build-Fehler.
  • Was der Browser erhält: Prerender → die beim Build erzeugte Datei (mit einem ETag); Server → jetzt gerendertes HTML; Client → index.csr.html, die leere Shell, ebenfalls von deinem Server ausgeliefert.
LiveWelcher Render-Modus für diese Route? (Simulation)
Beispiele:
EmpfehlungRenderMode.PrerenderSSG beim Build + SSR für den Rest
  • Das HTML ist für alle identisch und stabil: Du erzeugst es einmal, beim Build.
  • Als statische Datei ausgeliefert (idealerweise von einem CDN): minimaler TTFB, fast keine Kosten pro Request.
Achtung bei
  • Die Daten ändern sich täglich → du brauchst einen geplanten Build (z. B. nächtlich in der CI), der die Seiten neu erzeugt.
  • Tausende Seiten verlängern den Build um Minuten; miss die Build-Dauer und die Größe des Deployments.
import { PrerenderFallback, RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  {
    path: 'produse/:id',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const api = inject(CatalogApi); // synchron, vor dem await
      const items = await api.mostVisited(500);
      return items.map((x) => ({ id: String(x.id) }));
    },
    fallback: PrerenderFallback.Server,
  },
  // ...
];
Probiere die Beispiele aus oder beschreibe eine eigene Route. Die Empfehlung und der Eintrag in app.routes.server.ts aktualisieren sich live; „Achtung bei“ zeigt die versteckten Kosten der Wahl.

Header, Status und Redirects

Jede ServerRoute kann headers und status haben. Die Ausnahme: Prerender-Routen akzeptieren headers, aber nicht status — der Typ ServerRoutePrerender schließt ihn aus, weil die Datei nur einmal erzeugt wird. Die Header einer prerenderten Seite gelten nur, wenn sie über den Node-Server ausgeliefert wird, nicht von einem statischen Hosting.

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

export const serverRoutes: ServerRoute[] = [
  {
    // Öffentliches HTML: Das CDN darf es 60 s behalten und die alte Version ausliefern, während es aktualisiert
    path: 'cautare',
    renderMode: RenderMode.Server,
    headers: { 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300' },
  },
  {
    // permanenter Redirect: { path: 'oferte', redirectTo: 'promotii' } in app.routes.ts
    path: 'oferte',
    renderMode: RenderMode.Server,
    status: 301, // ohne status: 302
  },
  {
    // Prerender: headers ja, status NEIN (der Typ ServerRoutePrerender schließt ihn aus)
    path: 'despre',
    renderMode: RenderMode.Prerender,
    headers: { 'X-Frame-Options': 'DENY' },
  },
  { path: '**', renderMode: RenderMode.Server },
];

Ein redirectTo aus dem Router wird auf dem Server zu einem echten HTTP-Redirect (standardmäßig 302; du kannst status: 301, 303, 307 oder 308 setzen), beantwortet, ohne die Anwendung zu rendern. Ebenso unterscheidet sich die finale URL von der angefragten, wenn ein Guard während des Renderings umleitet (siehe Guards & Resolver), und der Server antwortet mit einem Redirect. Mit outputMode: 'static', wo es keinen Server gibt, werden Redirects als „Soft Redirects“ mit einem <meta http-equiv="refresh"> im HTML prerendert.

Status und Header zur Laufzeit festlegen

Hängt der Status von Daten ab (das Produkt existiert nicht), setzt du ihn in der Komponente über RESPONSE_INIT. Für Cookies, Sprache oder User-Agent liest du den Request über REQUEST. Beide stammen aus @angular/core und sind im Browser, beim Prerendering und beim Build null — behandle den null-Fall immer.

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

@Component({
  selector: 'app-not-found',
  template: `<h1>Seite nicht gefunden</h1>`,
})
export default class NotFound {
  constructor() {
    // RESPONSE_INIT ist im Browser, beim Prerendering und beim Build null → immer prüfen.
    const response = inject(RESPONSE_INIT);
    if (response) response.status = 404; // der Crawler sieht 404, nicht „200 mit einem Fehlertext“
  }
}

Prerendering mit Parametern

Routen ohne Parameter werden automatisch erkannt und prerendert. Bei produse/:id kann Angular nicht erraten, welche IDs existieren, also gibst du ihm eine Funktion getPrerenderParams. Sie läuft beim Build, in einem Injection-Kontext (du kannst inject() verwenden), und liefert eine Liste von Objekten Parameter → Wert. Eine Prerender-Route mit Parametern, aber ohne getPrerenderParams, bricht den Build ab.

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); // ✓ synchrones inject(), VOR jedem await
      const ids = await api.topProductIds(500); // läuft beim BUILD, nicht beim Request
      return ids.map((id) => ({ id: String(id) })); // die Werte müssen Strings sein
    },
    // /produse/9999 wurde nicht prerendert → was tun wir beim Request?
    fallback: PrerenderFallback.Server, // Standard: SSR beim Request
  },
  {
    path: 'blog/:slug/**', // Parameter + Catch-all
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      return [{ slug: 'ssr', '**': 'partea/1' }]; // → /blog/ssr/partea/1
    },
  },
  { path: '**', renderMode: RenderMode.Server },
];

Was mit einer URL passiert, die beim Build nicht erzeugt wurde, hängt von fallback ab:

PrerenderFallbackFür /produse/9999 (nicht prerendert)Wann du ihn wählst
Server (Standard)auf Anfrage auf dem Server gerendertgroßer Katalog: die Top N prerendern, den Rest auf Anfrage
Clienterhält die CSR-Shell, im Browser gerendertdu willst SEO nur für die erzeugten Seiten und kein SSR
NoneAngular antwortet nicht (der Request läuft weiter, meist ein 404)die Liste aus dem Build ist per Definition vollständig
inject() nach await

inject() funktioniert nur synchron, am Anfang der Funktion. Ein inject() nach einem await befindet sich nicht mehr in einem Injection-Kontext und wirft einen Fehler. Hole alle Abhängigkeiten in den ersten Zeilen und führe erst danach die Requests aus.

Die Kosten fallen beim Build an: Jede URL ist ein vollständiges Rendering. Zehntausend Produkte bedeuten zehntausend Renderings bei jedem Deployment und zehntausend hochzuladende Dateien. Ändern sich die Daten täglich, brauchst du außerdem einen geplanten Build, der sie neu erzeugt.

Deployment, Cache und Kosten

outputMode in angular.json bestimmt, was der Build erzeugt. Mit "server" (was ng add setzt) erhältst du dist/app/browser plus dist/app/server/server.mjs und brauchst einen Node-Prozess. Mit "static" erhältst du nur Dateien: prerenderte Seiten und die CSR-Shell, auf jedem CDN deploybar. Eine RenderMode.Server-Route im statischen Modus ist ein Build-Fehler.

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

Die Cache-Schichten

WasWieAchtung
JS, CSS, Bilder mit HashmaxAge: '1y' in express.static / CDNdie Namen ändern sich bei jedem Build, ein langer Cache ist also sicher
Prerendertes HTMLstatische Datei, ETag → 304 Not Modifiedändert sich nur beim Deployment
Öffentliches SSR-HTMLheaders pro Route: public, s-maxage=60, stale-while-revalidate=300nur für Inhalte, die für alle identisch sind
Persönliches SSR-HTMLprivate, no-storeniemals public: das CDN würde es einem anderen Nutzer ausliefern
HTTP-Daten beim RenderingTransfer Cache: GET/HEAD-Antworten landen im HTML und werden bei der Hydration wiederverwendetRequests mit Authorization/Cookie sind standardmäßig ausgeschlossen

Der Transfer Cache ist der Grund, warum SSR in der Timeline den API-Request nicht mehr vom Browser aus macht: Die vom HttpClient auf dem Server geholten Daten werden in <script id="ng-state"> serialisiert und beim Start gelesen. Seine Optionen (withHttpTransferCacheOptions, withNoHttpTransferCache) stehen auf der Seite Hydration. Außerdem gilt: Auf dem Server ist mit dem Standard-Fetch-Backend jede HTTP-Antwort auf 1 MB begrenzt (Fehler NG02825); das Limit änderst du mit provideServerRendering({ maxResponseBodySize }, …).

Betriebskosten

  • CPU pro Request: Jedes SSR-Rendering ist ein vollständiger Bootstrap. Ohne CDN-Cache bezahlst du den Bot-Traffic in Servern.
  • TTFB an das Backend gekoppelt: Der Server wartet auf die Stabilität, daher verlangsamt eine langsame API alle deine Seiten. Setze Timeouts und verschiebe sekundäre Daten in @defer (auf dem Server rendert ein Block ohne hydrate-Trigger nur den Platzhalter).
  • Zwischen Requests geteilter Speicher: Der Code wird einmal pro Prozess ausgewertet, daher gelten Variablen auf Modulebene und useValue-Provider für alle Nutzer gemeinsam.
  • Ein System mehr in Produktion: Logs, Monitoring, Skalierung, Node-Version, Cold Start bei Serverless.
import { InjectionToken } from '@angular/core';

// ✗ Zustand auf Modulebene: Auf dem Server wird das Modul EINMAL ausgewertet,
//    die Variable gilt also für alle Requests (und alle Nutzer).
let currentUser: string | null = null;

// ✗ useValue auf Anwendungsebene: derselbe Wert bis zum Neustart des Servers.
export const RENDERED_AT = new InjectionToken<number>('RENDERED_AT');
const bad = { provide: RENDERED_AT, useValue: Date.now() };

// ✓ useFactory: läuft bei jedem Rendering (jeder Request hat seinen eigenen Injector).
const good = { provide: RENDERED_AT, useFactory: () => Date.now() };

// ✓ Zustand pro Nutzer gehört in Services (pro Request instanziiert), nicht in globale Variablen.

Wann ja, wann nein

SSR / SSG lohnt sich
  • Öffentliche Seiten, die indexiert und geteilt werden sollen: Produkte, Artikel, Landing Pages. Prerender, wenn du sie beim Build erzeugen kannst, sonst Server.
  • Der LCP schlägt sich in Geld nieder (E-Commerce, Medien), und manche Nutzer haben einfache Smartphones und Netze.
  • Stabile Inhalte (Dokumentation, Blog, Marketing): Prerender + outputMode: 'static', ganz ohne Server, den du warten musst.
  • Öffentliche Inhalte mit frischen Daten und viel Traffic: Server + ein kurzer Cache im CDN.
  • Du brauchst korrekte Statuscodes (404, 301) für SEO.
Bleib bei CSR (oder wähle etwas anderes)
  • Anwendungen hinter dem Login (Dashboard, Admin, Backoffice): Client. Investiere in ein kleines Bundle, Lazy Routes und @defer.
  • Du hast niemanden, der einen Node-Server betreiben kann: Bleib bei CSR oder bei Prerender mit outputMode: 'static'.
  • „Die Anwendung ist nach dem Laden langsam“: SSR hilft nach der Hydration nicht. Das Problem liegt im JS oder in der Change Detection → Performance, Change Detection.
  • Routen, die auf Browser-APIs aufbauen (Karten, Canvas, Editoren): RenderMode.Client für diese Route.
  • Personalisiertes HTML bei viel Traffic: nicht pro Nutzer rendern. Den gemeinsamen Teil prerendern und die persönlichen Teile im Browser laden.

Häufige Stolperfallen

window is not defined

Code, der im Konstruktor, in ngOnInit oder auf Modulebene auf window, document oder localStorage zugreift, stürzt auf dem Server ab. Die Lösung: afterNextRender, das DOCUMENT-Token und getrennte Service-Implementierungen für Server und Browser — alles auf der Seite Server-kompatibler Code.

Zwischen Nutzern vermischte Daten

Eine Variable let currentUser auf Modulebene oder ein beim Start berechnetes useValue lebt so lange wie der Node-Prozess, nicht wie der Request. Lege den Zustand in Services ab (pro Rendering instanziiert) und nutze useFactory für Werte, die pro Request berechnet werden.

  • Öffentlicher Cache auf personalisiertem HTML → ein Nutzer sieht die Seite eines anderen. Verwende bei Server-Routen mit persönlichen Daten Cache-Control: private, no-store.
  • Leeres allowedHosts in Produktion → 400 auf allen Seiten (v22). Konfiguriere die Domains oder NG_ALLOWED_HOSTS.
  • „Soft 404“: Die „Not found“-Komponente antwortet mit 200. Setze status = 404 über RESPONSE_INIT.
  • Falscher SPA-Fallback bei statischem Hosting: Mit Prerendering ist index.html die Seite der Route /. Client-Routen müssen auf index.csr.html zurückfallen.
  • Die Anwendung wird nicht stabil: Ein PendingTask ohne Cleanup (oder, mit Zone.js, ein in der Angular-Zone gestartetes setInterval) hält das Rendering an, und der Request endet nie.
  • Ein CDN, das das HTML minifiziert und Kommentare entfernt → die Hydration scheitert mit NG0507. Die Kommentare von Angular werden gebraucht; deaktiviere die Option im CDN.
  • „Ich habe SSR eingebaut, und die TTI ist gleich geblieben“ — das ist normal: Das JS ist dasselbe. Miss den LCP, nicht die TTI, und reduziere das initiale JS separat.

Spickzettel

// 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)); // Standard 1 MB

// 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 beim Build (SSG)
  {
    path: 'produse/:id',
    renderMode: RenderMode.Prerender,
    getPrerenderParams: async () => [{ id: '1' }, { id: '2' }], // läuft beim Build
    fallback: PrerenderFallback.Server, // | Client | None — Standard: Server
  },
  { path: 'cautare', renderMode: RenderMode.Server, headers: { 'Cache-Control': 'no-store' } },
  { path: 'cont/**', renderMode: RenderMode.Client }, // leere Shell, im Browser gerendert
  { path: '**', renderMode: RenderMode.Server }, // das Sicherheitsnetz
];

// @angular/core — alle null im Browser, beim Prerendering und beim Build
inject(REQUEST); // Request: Header, Cookies, URL
inject(RESPONSE_INIT); // ResponseInit: dynamisch gesetzter Status / Header
inject(REQUEST_CONTEXT); // das zweite Argument von angularApp.handle(req, context)

// angular.json → architect.build.options
//   "outputMode": "server" | "static"               static = kein Node: nur Prerender + Client
//   "security": { "allowedHosts": ["exemplu.ro"] }  unbekannter Host → 400 Bad Request (v22)

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