34 Rendering-Konzepte in Angular: Von SSR bis Prerendering

34.1 Einführung in Rendering-Konzepte bei Angular

Bei der Entwicklung moderner Webanwendungen mit Angular stehen verschiedene Rendering-Strategien zur Verfügung, die jeweils spezifische Vor- und Nachteile bieten. Die Wahl der richtigen Strategie kann erheblichen Einfluss auf Leistung, Benutzererfahrung und Suchmaschinenoptimierung haben.

34.1.1 Client-Side Rendering (CSR)

Bei herkömmlichen Single-Page-Applications (SPAs) wie den meisten Angular-Anwendungen wird die gesamte Anwendung im Browser des Benutzers gerendert. Der Server liefert zunächst nur ein minimales HTML-Gerüst und JavaScript-Dateien aus. Der Browser führt dann das JavaScript aus, welches die DOM-Manipulation übernimmt und die Seite dynamisch aufbaut. Dies wird als Client Side Rendering (CSR) bezeichnet.

34.1.2 Server-Side Rendering (SSR)

Im Gegensatz dazu wird bei Server Side Rendering die Anwendung zunächst auf dem Server gerendert und als vollständiges HTML an den Browser gesendet. Der Browser kann die Seite sofort anzeigen, noch bevor Angular im Browser vollständig initialisiert ist. Nach dem Laden der JavaScript-Dateien übernimmt Angular die Kontrolle und macht die Anwendung interaktiv - ein Prozess, der als “Hydration” bezeichnet wird.

34.1.3 Prerendering

Prerendering ist ein Prozess, bei dem Angular-Anwendungen bereits zum Build-Zeitpunkt gerendert werden, anstatt erst im Browser des Benutzers oder bei jeder Anfrage auf dem Server. Dies ist besonders nützlich für Inhalte, die sich nicht häufig ändern.

34.2 Vorteile fortschrittlicher Rendering-Strategien

34.2.1 Verbesserte Benutzererfahrung

  1. Schnellere Erstladezeit: Benutzer sehen schneller Inhalte, da bereits gerenderte HTML-Seiten angezeigt werden.
  2. Reduzierte Belastung auf Client-Geräten: Die Rechenleistung des Servers wird genutzt, was besonders auf leistungsschwächeren Geräten vorteilhaft sein kann.
  3. Progressive Bootstrapping: Die Anwendung wird schrittweise geladen, beginnend mit dem kritischen Inhalt.

34.2.2 Suchmaschinenoptimierung (SEO)

  1. Bessere Indexierung: Suchmaschinen können den Inhalt Ihrer Seite besser analysieren, da das vollständige HTML bereits beim ersten Laden zur Verfügung steht.
  2. Verbesserte Social Media Previews: Bei Verwendung von Social Media Preview Cards werden die Meta-Tags bereits im ersten HTML enthalten sein.

34.2.3 Performance-Optimierung

  1. Reduzierte Time-to-Interactive: Durch die Kombination von sofortiger Darstellung und progressiver Hydration.
  2. Optimierte Ressourcennutzung: Server-Ressourcen werden für das initiale Rendering genutzt, Client-Ressourcen für die Interaktivität.

34.3 Die Evolution des Renderings in Angular

34.3.1 Angular 2-4: Frühe Server-Side Rendering mit Angular Universal

Angular Universal war die erste Lösung für serverseitiges Rendering in Angular. Sie ermöglichte es, Angular-Anwendungen auf dem Server vorzurendern und das generierte HTML an den Client zu senden.

// server.ts (Angular Universal Beispiel)
import 'zone.js/dist/zone-node';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { AppServerModule } from './src/main.server';

const app = express();

// Unser Angular Universal Engine
app.engine('html', ngExpressEngine({
  bootstrap: AppServerModule,
}));

app.set('view engine', 'html');
app.set('views', 'dist/browser');

// Statische Dateien aus /browser
app.get('*.*', express.static('dist/browser'));

// Alle Anfragen an den Angular app
app.get('*', (req, res) => {
  res.render('index', { req });
});

app.listen(4000, () => {
  console.log('Angular Universal Server läuft auf http://localhost:4000');
});

Die Implementierung war jedoch komplex und erforderte zusätzliche Server-Infrastruktur.

34.3.2 Angular 5-8: Verbesserungen an Angular Universal

In diesen Versionen wurde Angular Universal kontinuierlich verbessert:

// Beispiel für TransferState in Angular 7
import { Component, OnInit } from '@angular/core';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { HttpClient } from '@angular/common/http';

const DATA_KEY = makeStateKey('myData');

@Component({...})
export class AppComponent implements OnInit {
  constructor(
    private http: HttpClient,
    private state: TransferState
  ) {}

  ngOnInit() {
    // Prüfe, ob die Daten bereits vom Server vorhanden sind
    const storedData = this.state.get(DATA_KEY, null);
    
    if (storedData) {
      // Verwende bereits gerenderte Daten
      this.data = storedData;
    } else {
      // Hole die Daten und speichere sie für den Client
      this.http.get('/api/data').subscribe(data => {
        this.data = data;
        this.state.set(DATA_KEY, data);
      });
    }
  }
}

34.3.3 Angular 9-10: Ivy Renderer und verbesserte Performance

Mit der Einführung des Ivy-Renderers in Angular 9 verbesserte sich die Pre-Rendering-Performance deutlich:

Die Pre-Rendering-Fähigkeiten profitierten stark von diesen Verbesserungen, da die Anwendungen schneller kompiliert und gerendert werden konnten.

34.3.4 Angular 11-12: Verbessertes SSR-Setup

Angular 11 und 12 brachten bedeutende Verbesserungen für das Server-Side Rendering:

# Setup für SSR in Angular 12
ng add @nguniversal/express-engine

34.3.5 Angular 13-14: Hydration und schnellere Builds

Mit Angular 13 und 14 wurden die ersten Schritte in Richtung Hydration unternommen:

34.3.6 Angular 15-16: Einführung von Angular Hydration

Angular 15 führte die erste offizielle Unterstützung für Hydration ein, die in Angular 16 stark verbessert wurde:

// main.ts in Angular 16
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  ...appConfig,
  // Hydration aktivieren
  providers: [
    provideClientHydration()
  ]
}).catch(err => console.error(err));

Dies war ein großer Fortschritt, da es: - Die Interaktivität der Anwendung beschleunigte - Flackern beim Laden vermied - Den Hydration-Prozess automatisierte

34.3.7 Angular 17: Server-Side Rendering Revolution

Angular 17 brachte eine komplette Überarbeitung des SSR-Ansatzes:

// app.config.ts in Angular 17
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideClientHydration()
  ]
};

Die neue @defer-Direktive in Angular 17 ergänzte das Pre-Rendering perfekt:

<!-- Beispiel für deferred Loading in Angular 17 -->
<section>
  <header>Kritische Inhalte werden sofort gerendert</header>
  
  @defer {
    <heavy-component>
      Dieser Inhalt wird erst geladen, wenn er benötigt wird
    </heavy-component>
  }
</section>

34.3.8 Angular 18: Server-Side Rendering 2.0 und statisches Pre-Rendering

Angular 18 hat das Pre-Rendering auf eine neue Ebene gehoben:

  1. Verbessertes statisches Pre-Rendering: Bessere Unterstützung für statisches Pre-Rendering
// angular.json Konfiguration für statisches Pre-Rendering
{
  "projects": {
    "my-app": {
      "architect": {
        "prerender": {
          "builder": "@angular-devkit/build-angular:prerender",
          "options": {
            "routes": [
              "/",
              "/about",
              "/features"
            ]
          }
        }
      }
    }
  }
}
  1. Verbesserte Hydration: Feinere Kontrolle über den Hydration-Prozess
// app.config.ts in Angular 18
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration, withHttpTransferCacheOptions } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    provideClientHydration(
      withHttpTransferCacheOptions({
        includePostRequests: true
      })
    )
  ]
};
  1. Dynamische Pre-Rendering-API: Ermöglicht das Pre-Rendering basierend auf Daten
  2. Performance-Optimierungen: Verbesserte Build-Zeiten und Rendering-Performance
  3. Automatische Inhaltsoptimierung: Intelligente Optimierung von Assets und Inhalten während des Pre-Renderings

34.4 Angular Universal - SSR für Angular in der Praxis

Angular Universal ist die offizielle Implementierung von Server Side Rendering für Angular. Es ermöglicht das Rendern von Angular-Anwendungen auf dem Server.

34.4.1 Einrichtung von Angular Universal

Die Einrichtung von Angular Universal in einem bestehenden Angular-Projekt ist dank des Angular CLI relativ einfach geworden:

ng add @nguniversal/express-engine

Dieser Befehl führt mehrere Änderungen an Ihrem Projekt durch:

  1. Er erstellt zwei neue Eintrittspunkte (Entry Points):

  2. Er aktualisiert Ihre angular.json mit neuen Build-Konfigurationen für den Server.

  3. Er modifiziert Ihre app.module.ts und erstellt eine neue app.server.module.ts.

Nach der Installation können Sie Ihre Anwendung mit SSR mit dem folgenden Befehl ausführen:

npm run dev:ssr

34.4.2 Funktionsweise von Angular Universal

Wenn eine Anfrage an den Server eingeht, geschieht Folgendes:

  1. Der Express-Server leitet die Anfrage an Angular Universal weiter.
  2. Angular Universal rendert die entsprechende Angular-Komponente basierend auf der URL.
  3. Das generierte HTML wird zum Browser gesendet.
  4. Der Browser zeigt das HTML sofort an.
  5. Im Hintergrund werden die Angular-JavaScript-Dateien geladen und ausgeführt.
  6. Sobald Angular im Browser geladen ist, übernimmt es die Kontrolle über die Anwendung (Hydration).

34.5 Implementierungsdetails und Best Practices

34.5.1 TransferState API für Datenaustausch

Ein häufiges Problem bei SSR ist, dass API-Anfragen zweimal ausgeführt werden: einmal auf dem Server und einmal im Browser. Die TransferState API löst dieses Problem, indem sie Daten vom Server zum Client überträgt:

import { TransferState, makeStateKey } from '@angular/platform-browser';
import { isPlatformServer } from '@angular/common';
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

const HEROES_KEY = makeStateKey<Hero[]>('heroes');

@Injectable()
export class HeroService {
   constructor(
           private http: HttpClient,
           private transferState: TransferState,
           @Inject(PLATFORM_ID) private platformId: Object
   ) {}

   getHeroes(): Observable<Hero[]> {
      // Prüfen, ob Daten bereits im State vorhanden sind
      if (this.transferState.hasKey(HEROES_KEY)) {
         const heroes = this.transferState.get(HEROES_KEY, []);
         // Daten aus dem State löschen, damit sie bei Navigation nicht wiederverwendet werden
         this.transferState.remove(HEROES_KEY);
         return of(heroes);
      }

      // Wenn keine Daten im State, dann API-Anfrage durchführen
      return this.http.get<Hero[]>('/api/heroes').pipe(
              tap(heroes => {
                 // Daten im State speichern, wenn wir auf dem Server sind
                 if (isPlatformServer(this.platformId)) {
                    this.transferState.set(HEROES_KEY, heroes);
                 }
              })
      );
   }
}

34.5.2 Plattformspezifischer Code

Da Angular Universal sowohl auf dem Server als auch im Browser läuft, gibt es einige Unterschiede in der Umgebung. Zum Beispiel sind window, document und localStorage auf dem Server nicht verfügbar. Um damit umzugehen, können Sie überprüfen, auf welcher Plattform Ihr Code ausgeführt wird:

import { PLATFORM_ID, Inject } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';

@Component({
  selector: 'app-my-component',
  template: `...`
})
export class MyComponent {
  constructor(@Inject(PLATFORM_ID) private platformId: Object) {
    if (isPlatformBrowser(this.platformId)) {
      // Nur im Browser ausführen
      console.log('Läuft im Browser');
      localStorage.setItem('key', 'value');
    }

    if (isPlatformServer(this.platformId)) {
      // Nur auf dem Server ausführen
      console.log('Läuft auf dem Server');
    }
  }
}

34.5.3 Domino für DOM-Manipulation auf dem Server

Für Bibliotheken, die DOM-Manipulation erfordern, auch wenn sie auf dem Server laufen, kann Domino verwendet werden, um eine browserähnliche Umgebung auf dem Server zu simulieren:

// server.ts
import 'zone.js/dist/zone-node';
import * as domino from 'domino';
import * as fs from 'fs';

const template = fs.readFileSync('dist/browser/index.html').toString();
const win = domino.createWindow(template);

global['window'] = win;
global['document'] = win.document;
global['navigator'] = win.navigator;

34.5.4 Umgang mit externen Bibliotheken

Externe Bibliotheken können in einer SSR-Umgebung problematisch sein, besonders wenn sie stark vom DOM abhängig sind. Hier sind einige Ansätze:

  1. Verzögertes Laden (Lazy Loading): Laden Sie DOM-abhängige Bibliotheken nur auf der Client-Seite.
// Nur laden, wenn im Browser
if (isPlatformBrowser(this.platformId)) {
  import('some-dom-library').then(module => {
    // Mit der Bibliothek arbeiten
  });
}
  1. Angebots-/Mock-Implementierungen: Erstellen Sie serverseitige Versionen von Bibliotheken.
// my-library.service.ts
@Injectable()
export class MyLibraryService {
  constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

  doSomething() {
    if (isPlatformBrowser(this.platformId)) {
      // Echte Implementierung für Browser
      return window.someLibrary.doSomething();
    } else {
      // Server-Mock oder alternative Implementierung
      return 'Serverseite Simulation des Ergebnisses';
    }
  }
}

34.6 Hybride Rendering-Strategien

Angular unterstützt auch hybride Rendering-Strategien wie:

34.6.1 Statisches Prerendering

Wenn Ihre Inhalte nicht häufig aktualisiert werden, können Sie auch das Prerendering verwenden. Hierbei werden HTML-Seiten während des Build-Prozesses generiert und dann als statische Dateien bereitgestellt:

ng run your-app:prerender

Dies ist besonders nützlich für Blogs oder Dokumentationsseiten.

34.6.2 Incrementelle Static Regeneration (ISR)

Eine Kombination aus statischem Prerendering und dynamischer Aktualisierung. Die Seiten werden vorgerendert, aber in bestimmten Intervallen oder bei bestimmten Ereignissen neu generiert.

34.6.3 Dynamic Rendering

Entscheidung über SSR oder CSR basierend auf Benutzeragent oder anderen Faktoren, beispielsweise um Suchmaschinen optimierte Inhalte zu liefern.

34.7 Optimierung der Nutzererfahrung

34.7.1 Progressive Bootstrapping

Ein Konzept, das eng mit SSR verbunden ist, ist Progressive Bootstrapping. Hierbei wird die Anwendung schrittweise geladen:

  1. Zuerst wird das serverseitig gerenderte HTML angezeigt (schnelles erstes Rendering).
  2. Dann werden kritische Styles und Skripte geladen (visuelle Vollständigkeit).
  3. Schließlich wird die vollständige Angular-Anwendung geladen (volle Interaktivität).
<!-- index.html -->
<head>
  <!-- Kritische CSS sofort laden -->
  <style>
    /* Inline kritisches CSS hier */
  </style>
  
  <!-- Nicht-kritische Styles verzögert laden -->
  <link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
</head>

34.7.2 Shell-Architektur

Die App-Shell-Architektur kombiniert SSR mit Progressive Web Apps (PWAs):

  1. Der Server rendert zunächst eine “Shell” der Anwendung mit Header, Footer und Navigation.
  2. Der dynamische Inhalt wird dann entweder vom Server gerendert oder auf dem Client nachgeladen.
  3. Die Shell wird im Service Worker gecached, sodass sie bei nachfolgenden Besuchen sofort verfügbar ist.
// app-routing.module.ts
const routes: Routes = [
  { path: '', component: AppShellComponent, pathMatch: 'full' },
  { path: 'content', loadChildren: () => import('./content/content.module').then(m => m.ContentModule) }
];

34.8 Debugging und Fehlerbehebung

Das Debugging von SSR-Anwendungen kann herausfordernd sein, da der Code in zwei verschiedenen Umgebungen ausgeführt wird:

34.8.1 Server-Logging

Implementieren Sie ein gutes Logging auf der Serverseite:

// server.ts
import * as winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'server-error.log', level: 'error' }),
    new winston.transports.Console()
  ]
});

// In Ihrem Server-Code
try {
  // Servercode
} catch (error) {
  logger.error('Serverfehler aufgetreten:', error);
}

34.8.2 Häufige Fehler und ihre Lösungen

  1. ReferenceError: window is not defined Lösung: Verwenden Sie die PLATFORM_ID-Injektion, um zu prüfen, ob Sie im Browser sind.

  2. Error: Platform not supported Lösung: Stellen Sie sicher, dass Ihre Anwendung und alle verwendeten Module mit SSR kompatibel sind.

  3. Doppelte API-Anfragen Lösung: Implementieren Sie die TransferState API.

34.9 Leistungsmessung

Um die Vorteile von SSR und Prerendering quantifizieren zu können, sollten Sie die Leistung Ihrer Anwendung messen:

34.9.1 Metrics

  1. First Contentful Paint (FCP): Misst, wann der erste Inhalt angezeigt wird.
  2. Time to Interactive (TTI): Misst, wann die Seite vollständig interaktiv wird.
  3. Total Blocking Time (TBT): Misst die Zeit, in der die Hauptthread blockiert ist.

34.9.2 Tools

  1. Lighthouse: Führt umfassende Leistungsanalysen durch.
  2. WebPageTest: Testet die Seitenladezeit unter verschiedenen Bedingungen.
  3. Chrome DevTools Performance Panel: Bietet detaillierte Einblicke in die Ladezeit.
# Lighthouse über CLI ausführen
npx lighthouse https://your-ssr-app.com --view

34.10 Integration mit anderen Angular-Funktionen

34.10.1 Angular Router

Der Angular Router funktioniert nahtlos mit SSR, erfordert jedoch einige besondere Überlegungen:

// app.server.module.ts
@NgModule({
  imports: [
    AppModule,
    ServerModule,
    ServerTransferStateModule,
    ModuleMapLoaderModule,
    RouterModule  // Wichtig für serverseitiges Routing
  ],
  bootstrap: [AppComponent],
})
export class AppServerModule {}

34.10.2 NgRx/Redux State Management

Wenn Sie NgRx verwenden, können Sie den Store-Zustand zwischen Server und Client übertragen:

// app.server.module.ts
import { ServerTransferStateModule } from '@angular/platform-server';
import { NgRxTransferStateModule } from '@ngrx/store/transfer-state';

@NgModule({
  imports: [
    // ...
    ServerTransferStateModule,
    NgRxTransferStateModule
  ]
})
export class AppServerModule {}

34.11 Deployment-Strategien

34.11.1 Node.js Server

Der klassische Ansatz ist die Bereitstellung mit einem Node.js-Server:

# Build für Produktion
ng build
ng run your-app:server:production

# Starten des Servers
node dist/server/main.js

34.11.2 Serverless Deployment

SSR kann auch in einer Serverless-Umgebung bereitgestellt werden:

// serverless.ts
import 'zone.js/dist/zone-node';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';
import { AppServerModule } from './src/main.server';

export const app = express();

app.engine('html', ngExpressEngine({
  bootstrap: AppServerModule
}));

app.set('view engine', 'html');
app.set('views', join(process.cwd(), 'dist/browser'));

app.get('*.*', express.static(join(process.cwd(), 'dist/browser')));

app.get('*', (req, res) => {
  res.render('index', { req });
});

export const handler = serverless(app);