Angular SSR Applikation mit nginx als reverse proxy und gzip Kompression
Server Side Rendering (SSR) mit Angular ermöglicht es, Anwendungen auf dem Server zu rendern und dann als HTML an den Client zu senden. Dies bietet Vorteile in Bezug auf die Suchmaschinenoptimierung und die Performance der Anwendung. Im Bezug auf die Konfiguration gibt es aber Unterschiede, insbesondere wenn nginx als reverse Proxy zum Einsatz kommt.
Wird die Angular Applikation direkt über den nginx-Server ausgeliefert, dann reicht eine einfache Konfiguration über die gzip_static Direktive direkt im nginx:
server {
...
gzip_static on;
gzip_proxied any;
gzip_vary on;
gunzip on;
gzip_types application/javascript
...
}
Die beim Build generierten JavaScript Dateien können wir auch direkt mit gzip komprimieren (Auszug aus der package.json):
"scripts": {
"gzip": "gzip --keep --recursive ./dist/browser/*.js",
"build:de": "ng build --configuration=production ng run app-web:server:production && npm run gzip"
}
Dient nginx aber nur als reverse proxy mit proxy_pass, dann ist die nginx-Konfiguration mit gzip_static unnötig und wir müssen direkt den Express Server von Angular bemühen.
Hier die relevante Änderung in server.ts
, die standardmäßig beim Hinzufügen von Server-Side Rendering (SSR) zu Angular erstellt wird:
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/browser');
server.get('*.js', (req, res, next) => {
if (!existsSync(`${distFolder}${req.url}.gz`) ||
req.headers['x-no-compression'] ||
!req.header('Accept-Encoding').includes('gzip')) {
return next();
}
req.url = `${req.url}.gz`;
res.set({
'Content-Type': 'text/javascript',
'Content-Encoding': 'gzip',
'Vary': 'Accept-Encoding',
'Cache-Control': 'max-age=31536000'
});
next();
});
}
Wir setzen hier voraus, dass die JavaScript Dateien im Build schon gzip-t wurden und sich im gleichen Verzeichnis wie die eigentliche JavaScript Datei befinden (beim zuvor gezeigtem Script aus der package.json passiert genau das).
Wenn ein GET-Request (server.get(...)) für eine JavaScript-Datei (*.js) eingeht, werden zunächst die erforderlichen Bedingungen überprüft, um festzustellen, ob die Datei ausgeliefert werden kann. Wenn die Bedingungen erfüllt sind, wird die Datei mit den entsprechenden Headern zurückgegeben:
- Mit
existsSync(`${distFolder}${req.url}.gz`
wird geprüft ob die gzip-te Datei existiert (existsSync aus dem "fs" modul). - Über den
Accept-Encoding
Request-Header prüfen wir, ob der Browser die gzip Compression überhaupt unterstützt (das machen aber alle modernen Browser). - Wir prüfen noch ob der Header
req.headers['x-no-compression']
gesetzt ist: x-no-compression ist zwar kein Standard-Header, der Header wird aber auch in demexpress-static-gzip
Projekt verwendet und es macht auch Sinn, für den Fall des Falles eine Möglichkeit zu haben, über die man die Kompression ausschalten kann.
Kurze Erläuterung zu den Response-Headern:
- "Content-Type" und "Content-Encoding" sind offensichtlich
- Vary mit Accept-Encoding: dieser Header weist Caching-Server und Proxys an, separate Kopien der komprimierten und unkomprimierten Versionen der JavaScript-Dateien zu speichern, basierend auf dem „Accept-Encoding“-Header des Clients. Dadurch wird sichergestellt, dass der Server in Abhängigkeit von den Fähigkeit des Clients, den Inhalt zu dekomprimieren, die geeignete Version des Inhalts bereitstellt. Allerdings, soweit ich das verstehe, müssten wir bei Cache-Control den Flag public setzen. Sonst dürfen die Dateien nicht auf Proxys/Caching-Servern gespeichert werden.
- Hier wäre noch zu klären (auch in Verbindung mit dem "Vary" Header) ob Cache-Control: max-age=31536000, immutable, public nicht besser geeignet wäre: Da die generierten JS-Dateien im Namen einen Cache-Breaker (oder Cache-Buster) enthalten, sollte es kein Problem sein den Header eben auf "public" und "immutable" zu setzen.