Overview
Server-Side Rendering (SSR) generates your Angular application’s HTML on the server, providing faster initial page loads, better SEO, and improved performance on low-powered devices.
Benefits of SSR
Better SEO Search engines can crawl fully-rendered pages with content.
Faster First Paint Users see content faster before JavaScript loads and executes.
Social Sharing Preview images and metadata work correctly on social platforms.
Low-End Devices Better performance on devices with limited processing power.
Setup
Adding SSR to Existing Application
Use Angular CLI to add SSR support:
This command:
Installs necessary dependencies
Creates server-side configuration files
Updates angular.json with server build configuration
Adds server entry point files
Manual Setup
For more control, set up SSR manually:
// server.ts
import 'zone.js/node' ;
import { APP_BASE_HREF } from '@angular/common' ;
import { CommonEngine } from '@angular/ssr' ;
import * as express from 'express' ;
import { fileURLToPath } from 'node:url' ;
import { dirname , join , resolve } from 'node:path' ;
import bootstrap from './src/main.server' ;
const serverDistFolder = dirname ( fileURLToPath ( import . meta . url ));
const browserDistFolder = resolve ( serverDistFolder , '../browser' );
const indexHtml = join ( browserDistFolder , 'index.html' );
const app = express ();
const commonEngine = new CommonEngine ();
// Serve static files
app . get ( '*.*' , express . static ( browserDistFolder , {
maxAge: '1y'
}));
// All regular routes use the Angular engine
app . get ( '*' , ( req , res , next ) => {
const { protocol , originalUrl , baseUrl , headers } = req ;
commonEngine
. render ({
bootstrap ,
documentFilePath: indexHtml ,
url: ` ${ protocol } :// ${ headers . host }${ originalUrl } ` ,
publicPath: browserDistFolder ,
providers: [
{ provide: APP_BASE_HREF , useValue: baseUrl }
],
})
. then (( html ) => res . send ( html ))
. catch (( err ) => next ( err ));
});
export default app ;
Server Configuration
Main Server Bootstrap
// main.server.ts
import { bootstrapApplication } from '@angular/platform-browser' ;
import { AppComponent } from './app/app.component' ;
import { config } from './app/app.config.server' ;
const bootstrap = () => bootstrapApplication ( AppComponent , config );
export default bootstrap ;
Server App Configuration
// app.config.server.ts
import { ApplicationConfig , mergeApplicationConfig } from '@angular/core' ;
import { provideServerRendering } from '@angular/platform-server' ;
import { appConfig } from './app.config' ;
const serverConfig : ApplicationConfig = {
providers: [
provideServerRendering ()
]
};
export const config = mergeApplicationConfig ( appConfig , serverConfig );
Rendering Applications
Using renderApplication
import { renderApplication } from '@angular/platform-server' ;
import { AppComponent } from './app/app.component' ;
import { appConfig } from './app/app.config' ;
async function render ( url : string , document : string ) : Promise < string > {
const html = await renderApplication ( AppComponent , {
appId: 'my-app' ,
document ,
url ,
providers: [
... appConfig . providers
]
});
return html ;
}
Using renderModule (NgModule)
import { renderModule } from '@angular/platform-server' ;
import { AppServerModule } from './app/app.server.module' ;
async function render ( url : string , document : string ) : Promise < string > {
const html = await renderModule ( AppServerModule , {
document ,
url ,
extraProviders: [
// Additional server-specific providers
]
});
return html ;
}
Use platform checks to run code conditionally.
import { Component , Inject , PLATFORM_ID } from '@angular/core' ;
import { isPlatformBrowser , isPlatformServer } from '@angular/common' ;
@ Component ({
selector: 'app-platform-aware' ,
template: `
<div>
<p>Platform: {{ platform }}</p>
<p *ngIf="isBrowser">Window width: {{ windowWidth }}</p>
</div>
`
})
export class PlatformAwareComponent {
platform : string ;
isBrowser : boolean ;
windowWidth ?: number ;
constructor (@ Inject ( PLATFORM_ID ) private platformId : object ) {
this . isBrowser = isPlatformBrowser ( platformId );
this . platform = this . isBrowser ? 'Browser' : 'Server' ;
if ( this . isBrowser ) {
this . windowWidth = window . innerWidth ;
}
}
}
Conditional Browser APIs
import { Component , Inject , PLATFORM_ID , OnInit } from '@angular/core' ;
import { isPlatformBrowser } from '@angular/common' ;
@ Component ({
selector: 'app-local-storage' ,
template: `
<div>
<input
[(ngModel)]="value"
(input)="save()"
placeholder="Type something..."
>
<p>Stored value: {{ storedValue }}</p>
</div>
`
})
export class LocalStorageComponent implements OnInit {
value = '' ;
storedValue = '' ;
private isBrowser : boolean ;
constructor (@ Inject ( PLATFORM_ID ) platformId : object ) {
this . isBrowser = isPlatformBrowser ( platformId );
}
ngOnInit () : void {
if ( this . isBrowser ) {
this . storedValue = localStorage . getItem ( 'myValue' ) || '' ;
this . value = this . storedValue ;
}
}
save () : void {
if ( this . isBrowser ) {
localStorage . setItem ( 'myValue' , this . value );
this . storedValue = this . value ;
}
}
}
HTTP and Data Transfer
TransferState
Avoid duplicate HTTP requests by transferring data from server to client.
import { Component , OnInit } from '@angular/core' ;
import {
makeStateKey ,
TransferState
} from '@angular/platform-browser' ;
import { HttpClient } from '@angular/common/http' ;
interface User {
id : number ;
name : string ;
email : string ;
}
const USERS_KEY = makeStateKey < User []>( 'users' );
@ Component ({
selector: 'app-users' ,
template: `
<div *ngIf="users">
<h2>Users</h2>
<ul>
<li *ngFor="let user of users">
{{ user.name }} - {{ user.email }}
</li>
</ul>
</div>
<p *ngIf="!users">Loading...</p>
`
})
export class UsersComponent implements OnInit {
users ?: User [];
constructor (
private http : HttpClient ,
private transferState : TransferState
) {}
ngOnInit () : void {
// Check if data exists in transfer state
const cachedUsers = this . transferState . get ( USERS_KEY , null );
if ( cachedUsers ) {
// Use cached data from server
this . users = cachedUsers ;
} else {
// Fetch data and store for transfer
this . http . get < User []>( '/api/users' ). subscribe ( users => {
this . users = users ;
this . transferState . set ( USERS_KEY , users );
});
}
}
}
HTTP Interceptor for TransferState
import { Injectable } from '@angular/core' ;
import {
HttpInterceptor ,
HttpRequest ,
HttpHandler ,
HttpEvent ,
HttpResponse
} from '@angular/common/http' ;
import { Observable , of } from 'rxjs' ;
import { tap } from 'rxjs/operators' ;
import { TransferState , makeStateKey } from '@angular/platform-browser' ;
@ Injectable ()
export class TransferStateInterceptor implements HttpInterceptor {
constructor ( private transferState : TransferState ) {}
intercept (
req : HttpRequest < any >,
next : HttpHandler
) : Observable < HttpEvent < any >> {
// Only cache GET requests
if ( req . method !== 'GET' ) {
return next . handle ( req );
}
const key = makeStateKey ( req . url );
const cachedResponse = this . transferState . get ( key , null );
if ( cachedResponse ) {
// Remove from transfer state after using
this . transferState . remove ( key );
return of ( new HttpResponse ({ body: cachedResponse , status: 200 }));
}
return next . handle ( req ). pipe (
tap ( event => {
if ( event instanceof HttpResponse ) {
this . transferState . set ( key , event . body );
}
})
);
}
}
Pre-rendering (SSG)
Generate static pages at build time for even better performance.
Configuration
// angular.json
{
"projects" : {
"my-app" : {
"architect" : {
"prerender" : {
"builder" : "@angular-devkit/build-angular:prerender" ,
"options" : {
"routes" : [
"/" ,
"/about" ,
"/contact" ,
"/blog/post-1" ,
"/blog/post-2"
]
},
"configurations" : {
"production" : {
"browserTarget" : "my-app:build:production" ,
"serverTarget" : "my-app:server:production"
}
}
}
}
}
}
}
Dynamic Route Discovery
// routes.txt or routes generator
import { writeFileSync } from 'fs' ;
import { join } from 'path' ;
interface BlogPost {
slug : string ;
}
async function generateRoutes () : Promise < void > {
// Fetch dynamic routes from API or database
const posts : BlogPost [] = await fetchBlogPosts ();
const routes = [
'/' ,
'/about' ,
'/contact' ,
... posts . map ( post => `/blog/ ${ post . slug } ` )
];
// Write routes to file
const routesFile = join ( __dirname , 'routes.txt' );
writeFileSync ( routesFile , routes . join ( ' \n ' ));
}
async function fetchBlogPosts () : Promise < BlogPost []> {
// Fetch from your API or database
return [];
}
generateRoutes ();
SEO Optimization
import { Injectable } from '@angular/core' ;
import { Meta , Title } from '@angular/platform-browser' ;
@ Injectable ({ providedIn: 'root' })
export class SeoService {
constructor (
private meta : Meta ,
private title : Title
) {}
updateMetaTags ( config : {
title : string ;
description : string ;
image ?: string ;
url ?: string ;
type ?: string ;
}) : void {
// Update title
this . title . setTitle ( config . title );
// Update meta tags
this . meta . updateTag ({ name: 'description' , content: config . description });
// Open Graph tags
this . meta . updateTag ({ property: 'og:title' , content: config . title });
this . meta . updateTag ({ property: 'og:description' , content: config . description });
this . meta . updateTag ({ property: 'og:type' , content: config . type || 'website' });
if ( config . image ) {
this . meta . updateTag ({ property: 'og:image' , content: config . image });
}
if ( config . url ) {
this . meta . updateTag ({ property: 'og:url' , content: config . url });
}
// Twitter Card tags
this . meta . updateTag ({ name: 'twitter:card' , content: 'summary_large_image' });
this . meta . updateTag ({ name: 'twitter:title' , content: config . title });
this . meta . updateTag ({ name: 'twitter:description' , content: config . description });
if ( config . image ) {
this . meta . updateTag ({ name: 'twitter:image' , content: config . image });
}
}
}
Using SEO Service
import { Component , OnInit } from '@angular/core' ;
import { ActivatedRoute } from '@angular/router' ;
import { SeoService } from './seo.service' ;
interface BlogPost {
title : string ;
description : string ;
image : string ;
slug : string ;
}
@ Component ({
selector: 'app-blog-post' ,
template: `
<article *ngIf="post">
<h1>{{ post.title }}</h1>
<img [src]="post.image" [alt]="post.title">
<p>{{ post.description }}</p>
</article>
`
})
export class BlogPostComponent implements OnInit {
post ?: BlogPost ;
constructor (
private route : ActivatedRoute ,
private seo : SeoService
) {}
ngOnInit () : void {
this . route . data . subscribe ( data => {
this . post = data [ 'post' ];
if ( this . post ) {
this . seo . updateMetaTags ({
title: this . post . title ,
description: this . post . description ,
image: this . post . image ,
url: `https://example.com/blog/ ${ this . post . slug } ` ,
type: 'article'
});
}
});
}
}
Best Practices
Use TransferState Avoid duplicate HTTP requests by transferring data from server to client.
Lazy Load Heavy Components Defer loading of non-critical components to improve initial render time.
Handle Platform Differences Always check platform before accessing browser-only APIs.
Optimize Images Use responsive images and lazy loading for better performance.
Avoid using browser-specific APIs like window, document, localStorage without platform checks. These will cause errors during server-side rendering.
Additional Resources