Angular SSR – Server Side Rendering in applicaties

Angular SSR – Server Side Rendering in applicaties

Auteur: Danny Holstein

Enige tijd geleden was mij de taak toegewezen om SEO (Search Engine Optimalization) te verbeteren voor een Angular applicatie. Over het algemeen blinken client-side JavaScript applicaties niet uit op het gebied van SEO, doordat de crawlers van de grote zoekmachines niet in staat zijn om JavaScript uit te voeren. Door dit laatste zullen zoekmachines pagina’s helemaal niet indexeren en dit is een nadeel voor de zichtbaarheid van een website of applicatie. In dit nieuwe blogbericht wordt het toepassen van Angular SSR (Server Side Rendering) in een bestaande applicatie nader behandeld.

Angular Universal en Angular SSR

Om SEO te verbeteren, boden eerdere versies van Angular de optie Angular Universal aan (dat is de voorloper van Angular SSR). Angular Universal is oorspronkelijk in 2016  geïntroduceerd, eerst als een community project en dit is later geadopteerd door Angular met een aantal grote verbeteringen. Een nadeel van Angular Universal is het instellen van de configuratie en veel online tutorials en handleidingen zijn verouderd. Angular Universal heeft ook problemen met libraries van derde partijen. Zelf heeft Angular Universal extra libraries nodig waarop gesteund wordt (onder andere @angular-platform-server, @nguniversal/express-engine, ts-loader en anderen). Angular Universal is extra complex doordat er geen ingebouwde environment ondersteuning is.

Volledige absorptie van Angular Universal heeft plaatsgevonden Angular 17 en deze library is samengevoegd met de CLI (Command Line Interface). Angular Universal is ook omgedoopt tot Angular SSR. De architectuur en kern functionaliteit is hetzelfde gebleven. Angular Universal is nu verouderd en wordt niet meer onderhouden (op een aantal LTS - Long Term Support - versies na). Het is tegenwoordig aanbevolen om weg te migreren van alle pakketten die betrekking hebben op Angular Universal.

Tussen Angular Universal en Angular SSR zijn er verschillende raakvlakken. Beide libraries dienen hetzelfde doel, namelijk SSR (Server Side Rendering). Er is ook dezelfde onderliggende architectuur waarbij het pakket @angular/platform-server het fundament is van beiden. Ook is de CommonEngine (daar is geen hybride rendering mogelijk) overgenomen, maar aanzienlijk verbeterd en voorzien van een andere naam: AngularNodeAppEngine. Andere overeenkomsten zijn dat Node.js wordt gebruikt om pagina’s vooraf weer te geven op de server en er is ondersteuning voor Hydration (zie volgende paragraaf).

Angular SSR - de theorie

Sinds Angular 17 wordt Angular SSR steeds verder ontwikkeld en voorzien van nieuwe features. Met Angular SSR wordt er 1 codebase geschreven die gecompileerd wordt in 2 bundles: de eerste is de server bundle en de tweede is de gebruikelijke client bundle. De SSR logica wordt uitgevoerd op een Node.js server die alle HTTP-requests ontvangt en de pagina voorbereidt en weergeeft op de server. Uiteindelijk wordt de volledig weergegeven HTML teruggegeven aan de browser. De browser toont de HTML meteen (fast initial load) en daarna gebeurd er het proces van hydration waarbij JavaScript wordt geladen en de pagina interactief wordt gemaakt.

Een officiëlere definitie van hydration is: “nadat de server de HTML naar de browser verstuurt, doet Angular’s client-side JavaScript de statische HTML ‘ontwaken’. Dit houdt in dat: event-listeners worden vastgemaakt, de data-bindings worden ingesteld, de pagina wordt interactief en vooraf weergegeven zaken worden behouden zonder flikkeringen. “ Een algemene analogie is als volgt: ‘de resurrectie van een plantje’: waarbij de ‘opgedroogde/verdorde staat’ van het plantje onder de woestijnzon vergelijkbaar is met de static HTML die aangeleverd wordt door de server – het plantje is ‘structureel compleet in vorm en verschijning, maar nog niet responsief’. Door water toe te voegen (hydration) komt het plantje terug tot leven – of met andere woorden: Angular brengt interactiviteit terug op de pagina.

Een van de voordelen van Angular SSR is dat er op een dynamische manier meta-tags zijn in te stellen, zoals: meta-title en meta-description. Dit is een groot voordeel voor SEO en de zichtbaarheid van pagina’s. Een andere voordeel is: een snellere initiële lading van de pagina en ook wat betreft social sharing zullen de ‘preview cards’ correct werken. Sociale media tags zijn: og:title, og:description, og:image, en og:url – dit zijn de zogenaamde graph tags, waarmee wordt gedefinieerd hoe de content getoond moet worden in social feeds. Een van de hoofdvoordelen van SSR is dat nog voordat de HTML naar de browser wordt gestuurd, er (meta)data opgehaald kan worden vanuit een WebAPI. Zoekmachines zullen deze dynamische gegenereerde meta-tags zien en indexeren.

Een andere hoofdreden om SSR te gebruiken heeft betrekking op de prestaties. Het uitvoeren van JavaScript kan een intensieve taak zijn voor de browser. Ook moeten eerst de scripts gedownload worden en dit is een belasting voor het netwerk. Dit laatste kan nadelig zijn voor vooral mobiele apparaten: die limiteringen hebben met de hardware en over het algemeen tragere verbindingen hebben. Het lokaal ontwikkelen van een Angular SSR applicatie komt in grote lijnen overeen met de theorie (vooral wanneer er gebruik gemaakt wordt van hard-gecodeerde data). Het is een grotere uitdaging om SSR toe te passen in een bestaande Angular applicatie die gebruik maakt van een WebApi.

Angular SSR toepassen in een bestaande applicatie – de praktijk

Ongeveer een jaar geleden heb ik geëxperimenteerd met een lokale Angular SSR applicatie. Zoals in de inleiding geschreven, had ik pasgeleden de kans om Angular SSR toe te passen in een al bestaande Angular applicatie. De eerste stap was om de applicatie in ieder geval te upgraden naar Angular versie 19, waardoor een belangrijke security fix beschikbaar is. Hierover heb ik in een eerder blogbericht geschreven (zie het blogbericht: ‘Angular 21 – een tussenstand en korte terugblik’). De fix hield in dat er potentiële lekken worden voorkomen.

Om later SSR toe te voegen aan een project, is er het commando: ng add @angular/ssr – en hiermee  genereert de Angular CLI extra bestanden die te herkennen aan het achtervoegsel server.ts. De volgende stap is om na te gaan welke pagina’s er weergegeven moeten worden door middel van SSR. Het is onzinnig om admin-pagina’s met SSR te weer te geven of pagina’s die alleen zichtbaar zijn voor ingelogde gebruikers, want deze pagina’s worden toch nooit bezocht door zoekmachines. Wel kunnen de publieke pagina’s worden weergegeven met SSR en dit zal dan ook SEO voordelen opleveren.

Dankzij de bovengenoemde AngularNodeAppEngine is er ‘hybride rendering’ mogelijk en dit wil zeggen dat bepaalde routes van de applicatie weergegeven worden op de server (de publiekelijk toegankelijke pagina’s) en de overige routes worden weergegeven in de browser (de admin-pagina’s en pagina’s die alleen zichtbaar zijn voor ingelogde gebruikers). Een belangrijke voorwaarde is wel dat AngularNodeAppEngine het meer recentere pakket @angular/build nodig heeft (in plaats van @angular-devkit/build-angular die deprecated is).

Wanneer een pagina op de server wordt weergegeven, dan moeten bepaalde stukken code worden afgeschermd op de server. Of met andere woorden: er wordt onderscheid gemaakt tussen welke code er op de server moet worden weergegeven en welke code er in de browser moet worden weergegeven. In Angular SSR is dit mogelijk met gebruikelijke combinatie van ‘platform id’ en de isPlaformBrowser methode. Zie de voorbeeldcode hieronder.

angular_ss_platformid_and_isPlatformBrowser

Code die wordt weergegeven op de server kan niet berusten op browser-specifieke API’s (deze zijn alleen beschikbaar in een browser en niet op de Node.js server). Veel gebruikte voorbeelden hiervan zijn: window, document, navigator en properties zoals HTMLElement. In Angular SSR zal dit runtime errors veroorzaken. In plaats daarvan moet gebruik worden gemaakt van ‘SSR abstracties’ die voorzien zijn door Angular en geschikt zijn voor SSR. Voor document heeft SSR ingebouwde functionaliteit, maar voor window is er een aparte injection token nodig.

Een andere limitering is het gebruik van libraries van derde partijen. Het moet dan zeker zijn dat deze niet alleen SSR compatibel zijn (of er bewust van zijn dat SSR wordt uitgevoerd), maar ook moeten deze libraries kunnen samenwerken met SSR. Problematisch zijn libraries die gebruik maken van de DOM en of van window, document, navigator, etc.

Een andere gebruikelijke waarschuwing wanneer er met Angular SSR wordt gewerkt dat is NG0506 – dat aangeeft dat de applicatie niet binnen 10 seconden stabiel kan worden. Dit laatste kan in een productieomgeving voor problemen gaan zorgen. Deze waarschuwing kan gewoonlijk worden veroorzaakt door zowel de JavaScript functies setInteval en setTimeout als de RxJS operatoren: interval en timer. De achterliggende oorzaak hiervan is vaak een HTTP-request dat nooit beëindigd wordt of ook een Promise die nooit wordt opgelost. Ook in de bestaande applicatie bleek dit het geval te zijn. Er werd in een bepaald stuk code gebruik gemaakt van de RxJS operator interval voor ‘polling’ (dit wil zeggen dat er na een bepaalde interval een HTTP-request wordt gedaan om nieuwe data op te halen).

angular_ssr_error_ng0506

Gelukkig zijn er sinds Angular 19 geavanceerdere features waarmee is aan te geven dat bepaalde code niet in SSR moet worden weergegeven. Een voorbeeld daarvan is de methode afterNextRender dat alleen in de browser en dus buiten SSR wordt uitgevoerd. De methode afterNextRender zal alleen in de browser worden uitgevoerd, nadat de hydration gecompleteerd is en dit voorkomt dat ‘polling’ ApplicationRef.isStable blokkeert. Een nadeel van deze methode is dat dit onder andere alleen beschikbaar is in een constructor.

angular_ssr_afterNextRender_example

Wanneer het ook de bedoeling is om op een bepaald punt Angular’s change detection niet over te halen, dan kan er binnen afterNextRender ook nog eens gebruik gemaakt worden van NgZone en dit biedt de mogelijkheid om code uit te voeren buiten de Angular change detection. Angular zal dan niets weten over deze code en zal geen change detection cycle uitvoeren en dit zal bovendien in bepaalde situaties de prestaties verbeteren. In de bestaande Angular applicatie bleek de combinatie van afterNextRender en NgZone uitkomst te bieden en de hierboven vermelde waarschuwing NG0506 werd hiermee opgelost.

Het is vaak niet mogelijk om een Angular applicatie te upgraden naar de meest recente versie (Angular 22 is uitgekomen op 3 juni 2026). Bepaalde libraries kunnen achterlopen en er kunnen brekende veranderingen zijn. Het kan ook te veel van het goede zijn om een bestaande (en naar behoren werkende applicaties) Zoneless te maken. Ook voor deze bestaande applicatie is besloten om juist verder gebruik te maken van Zone.js. Om bepaalde onderliggende problemen tussen Zone.js en RxJS te overbruggen tijdens Angular SSR is er een ‘monkey-patch’ beschikbaar (dat is een techniek die wordt gebruikt in dynamische programmeertalen om het gedrag van bestaande code of externe bibliotheken dynamisch aan te passen of uit te breiden tijdens de uitvoering, zonder de originele bronbestanden te wijzigen). Deze oplossing wordt in de polyfills array opgenomen in angular.json – zie het voorbeeld hieronder.

Met Angular SSR is er het gevaar dat er dubbele HTTP-requests worden verzonden naar de WebAPI. Dit laatste is te voorkomen door middel van state keys. Om de data op te halen kan er binnen Angular SSR gebruik worden gemaakt van resolvers om de data te laden. De resolvers zorgen ervoor dat het zeker is dat de data is geladen nog voordat deze wordt weergegeven. Angular resolvers worden toegevoegd aan publiekelijk toegankelijke routes.

angular_ssr_resolver_example_code

Wanneer Angular een pagina op de server weergeeft, haalt de server de gegevens op, wordt de HTML gegenereerd en deze naar de browser gestuurd. Zonder TransferState (zie de resolver in de afbeelding hier rechtsboven) zou de browser dezelfde gegevens opnieuw ophalen, wat een onnodige netwerkoproep zou betekenen en een flikkering zou veroorzaken. Deze resolver voorkomt dit door de server de reeds opgehaalde gegevens direct aan de browser te laten doorgeven.

In de code hier rechtsboven wordt eerst het id uit de route parameters opgehaald. Vervolgens wordt er een unieke transfer key gemaakt die opgeslagen wordt in de TransferState. Ook de id wordt opgenomen in de transfer key en dit leidt ertoe dat er geen verouderde data wordt geladen wanneer er naar een ander product wordt genavigeerd. Het eerste if-block is het pad van de browser en dit consumeert de overgedragen data - dit haalt de data op uit de TransferState, en verwijdert de transfer key onmiddellijk, en zal een Observable (zie of(product)) teruggeven. In de browser zal er geen tweede HTTP-request plaatsvinden. Onderaan deze code is er het server pad: wanneer de transfer key niet bestaat in de TransferState (en dat is altijd het geval op de server), dan zal de resolver de data ophalen vanuit de WebApi. De RxJS tap operator voert een zijeffect uit, en slaat het opgehaalde product op voordat de Observable completeert. Angular SSR serialiseert de data in een HTML-respons en maakt dit beschikbaar in de browser.

Met Angular SSR is er nog veel meer mogelijk. Zo is het ook mogelijk om gestructureerde data toe te voegen met JSON-LD (JavaScript Object Notation for Linked Data). Dit laatste is een methode om semantische opmaak toe te voegen aan een webpagina en dit helpt zoekmachines te begrijpen en meer effectief de content te categoriseren. Dit laatste leidt vaak tot verbeterde zoekresultaten en rijkere data. Met Angular SSR wordt er een apart script-block in het head element gebruikt. JSON-LD is een standaard die verschillende content-types beschrijft, door middel van gestandaardiseerde properties zoals: type, name, description, image, rating, en price (voor E-commerce). Bovendien heeft Google deze standaard expliciet aanbevolen.

Tijdens het lokaal toepassen van de code zijn bepaalde HTTP-requests naar de Node.js meerdere keren uitgetest. In het bijzonder wilde ik de output weten en in hoeverre de meta-tags, og-tags en de rich data werd toegevoegd. De syntax voor dit commando is (vanuit de VS Code terminal):

curl.exe --ssl-no-revoke -D - https://some-test-site.com/profile/15 -o profile_15.html 

Door middel van het hulpprogramma curl en bepaalde flags kan een link worden bezocht op https. De -o flag maakt een output bestand, waarin de response wordt uitgeschreven. Wanneer Angular SSR naar behoren werkt, dan zijn de meta-tags, og-tags en de rich data terug te zien in het bestand. Een voorbeeld hiervan is te zien in de onderstaande afbeelding.

Tenslotte is er het moment gekomen om de applicatie klaar te maken voor productie. Hierbij is het noodzakelijk dat sitemap.xml (wordt gebruikt door zoekmachines om pagina’s te indexeren) en robots.txt (wordt gebruikt door zoekmachines voor extra aanwijzingen en is ook noodzakelijk voor de rich data met JSON-LD) toegankelijk zijn voor de crawlers van de zoekmachines. Voor productie modus wordt er in de praktijk gebruik gemaakt van een Nginx server die dient als reverse proxy die bepaald of een HTTP-request naar de Angular SSR applicatie of naar een andere route wordt doorgeleid. Om een up-to-date sitemap te tonen, moet de Nginx server het verzoek doorgeleiden naar een specifiek endpoint van de WebApi. Via Nginx moeten ook bepaalde headers worden ingesteld, dat tijdens lokale ontwikkeling vaak niet nodig is. In de praktijk bleek er dus ook een groot verschil te zijn tussen lokale ontwikkeling en productie modus.

Tot slot

De wereld van Angular SSR is nog steeds complex en kan een uitdaging zijn om toe te passen in een bestaande Angular applicatie. Angular SSR is de laatste tijd aanzienlijk verbeterd, waarbij bepaalde zaken zijn vereenvoudigd en veel configuratie uit handen wordt genomen. Deze ontwikkeling zet zich steeds verder door en er wordt bij iedere nieuwe Angular versie nieuwe features toegevoegd. Een voorbeeld daarvan is incrementele hydratie (waarbij bepaalde delen van een pagina actief worden gemaakt en andere delen pas later actief worden) dat mogelijk is sinds de meest recente versies van Angular.

Wanneer Angular SSR wordt toegepast in een bestaande applicatie, dan wordt er veel gebruik gemaakt van andere functionaliteit (‘platform id’, isPlatformBrowser, afterNextRender, resolvers, etc.) die niet gebruikt wordt in reguliere browser applicaties. Er is ook een groot verschil tussen lokale ontwikkeling en het gereed maken voor productie modus. Een voordeel is dat er in recente Angular SSR applicaties geen gebruik gemaakt hoeft te worden van ‘polyfills’ in de code (aanvullende functionaliteit die in een browser kan ontbreken). Voor de uitdagingen die zich voordeden, was er ook een doelmatige oplossing beschikbaar binnen Angular. In ieder geval is er de nodige ervaring opgedaan en ik vond het interessant om Angular SSR in een bestaande applicatie toe te passen.