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.
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.
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.
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.
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.
In diesen Versionen wurde Angular Universal kontinuierlich verbessert:
TransferState für bessere
Datenverwaltung zwischen Server und Client// 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);
});
}
}
}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.
Angular 11 und 12 brachten bedeutende Verbesserungen für das Server-Side Rendering:
ng add @nguniversal/express-engine# Setup für SSR in Angular 12
ng add @nguniversal/express-engineMit Angular 13 und 14 wurden die ersten Schritte in Richtung Hydration unternommen:
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
Angular 17 brachte eine komplette Überarbeitung des SSR-Ansatzes:
@angular/ssr als neue, vereinfachte
API// 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>Angular 18 hat das Pre-Rendering auf eine neue Ebene gehoben:
// angular.json Konfiguration für statisches Pre-Rendering
{
"projects": {
"my-app": {
"architect": {
"prerender": {
"builder": "@angular-devkit/build-angular:prerender",
"options": {
"routes": [
"/",
"/about",
"/features"
]
}
}
}
}
}
}// 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
})
)
]
};Angular Universal ist die offizielle Implementierung von Server Side Rendering für Angular. Es ermöglicht das Rendern von Angular-Anwendungen auf dem Server.
Die Einrichtung von Angular Universal in einem bestehenden Angular-Projekt ist dank des Angular CLI relativ einfach geworden:
ng add @nguniversal/express-engineDieser Befehl führt mehrere Änderungen an Ihrem Projekt durch:
Er erstellt zwei neue Eintrittspunkte (Entry Points):
main.server.ts als Einstiegspunkt für die serverseitige
Anwendungserver.ts als Express-Server, der Ihre Anwendung
rendertEr aktualisiert Ihre angular.json mit neuen
Build-Konfigurationen für den Server.
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:ssrWenn eine Anfrage an den Server eingeht, geschieht Folgendes:
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);
}
})
);
}
}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');
}
}
}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;Externe Bibliotheken können in einer SSR-Umgebung problematisch sein, besonders wenn sie stark vom DOM abhängig sind. Hier sind einige Ansätze:
// Nur laden, wenn im Browser
if (isPlatformBrowser(this.platformId)) {
import('some-dom-library').then(module => {
// Mit der Bibliothek arbeiten
});
}// 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';
}
}
}Angular unterstützt auch hybride Rendering-Strategien wie:
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:prerenderDies ist besonders nützlich für Blogs oder Dokumentationsseiten.
Eine Kombination aus statischem Prerendering und dynamischer Aktualisierung. Die Seiten werden vorgerendert, aber in bestimmten Intervallen oder bei bestimmten Ereignissen neu generiert.
Entscheidung über SSR oder CSR basierend auf Benutzeragent oder anderen Faktoren, beispielsweise um Suchmaschinen optimierte Inhalte zu liefern.
Ein Konzept, das eng mit SSR verbunden ist, ist Progressive Bootstrapping. Hierbei wird die Anwendung schrittweise geladen:
<!-- 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>Die App-Shell-Architektur kombiniert SSR mit Progressive Web Apps (PWAs):
// app-routing.module.ts
const routes: Routes = [
{ path: '', component: AppShellComponent, pathMatch: 'full' },
{ path: 'content', loadChildren: () => import('./content/content.module').then(m => m.ContentModule) }
];Das Debugging von SSR-Anwendungen kann herausfordernd sein, da der Code in zwei verschiedenen Umgebungen ausgeführt wird:
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);
}ReferenceError: window is not defined Lösung: Verwenden Sie die PLATFORM_ID-Injektion, um zu prüfen, ob Sie im Browser sind.
Error: Platform not supported Lösung: Stellen Sie sicher, dass Ihre Anwendung und alle verwendeten Module mit SSR kompatibel sind.
Doppelte API-Anfragen Lösung: Implementieren Sie die TransferState API.
Um die Vorteile von SSR und Prerendering quantifizieren zu können, sollten Sie die Leistung Ihrer Anwendung messen:
# Lighthouse über CLI ausführen
npx lighthouse https://your-ssr-app.com --viewDer 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 {}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 {}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.jsSSR 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);