Server-Side Rendering
Server-Side Rendering (SSR) is a technique where the server generates HTML pages, unlike traditional Single Page Applications (SPAs) that render on the client. Angular SSR allows your Angular application to run on the server, generating static HTML, which offers benefits like improved SEO, faster First Contentful Paint (FCP), and enhanced user experience.
Angular SSR Principles
Angular SSR uses a Node.js environment to run the Angular application, compiling it into code executable on Node.js. When the server receives a request, it executes the Angular app, captures the rendered output, and sends the generated HTML to the client. This ensures the client sees a fully rendered page on initial load without waiting for JavaScript execution.
Preparing the Environment
To use Angular SSR, ensure Node.js and Angular CLI are installed in your development environment. Create a new project and add SSR support using Angular CLI.
ng new my-app --style=scss
cd my-app
ng add @nguniversal/express-engine --clientProject=my-appBuilding an SSR Application
After adding SSR support, use the build:ssr command to build the SSR version of the application.
ng build --prod
ng run my-app:server:productionThis generates two main outputs: a client-side application for the browser and an SSR application for the server.
Server-Side Configuration
To run an SSR application, you need a Node.js server. Angular SSR uses Express.js by default as the server framework. The src/server.ts file contains the server-side configuration.
// src/server.ts
import { enableProdMode } from '@angular/core';
import { platformServer } from '@angular/platform-server';
import { AppModule } from './app/app.module';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';
enableProdMode();
const app = express();
const DIST_FOLDER = join(process.cwd(), 'dist/browser');
app.engine(
'html',
ngExpressEngine({
bootstrap: AppModule,
})
);
app.set('view engine', 'html');
app.set('views', DIST_FOLDER);
// Serve static files from /browser
app.get('*.*', express.static(DIST_FOLDER, {
maxAge: '1y'
}));
// All regular routes use the Universal engine
app.get('*', (req, res) => {
res.render('index', { req });
});
// Start the Node server
const port = process.env.PORT || 4000;
app.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});Handling State Sharing
In SSR, state sharing between client and server is necessary. Angular provides the TransferState service to pass state from the server to the client.
// app.module.ts
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { BrowserModule, TransferState } from '@angular/platform-browser';
export function initializeApp(state: TransferState) {
return () => {
// Initialize your application here
};
}
@NgModule({
imports: [BrowserModule, /* other imports */],
providers: [
{ provide: APP_INITIALIZER, useFactory: initializeApp, deps: [TransferState], multi: true },
/* other providers */
],
/* other configurations */
})
export class AppModule {}Unit Testing SSR Applications
Testing SSR applications requires additional configuration since server code cannot run directly in a browser environment. Use Jest or Mocha with Supertest to simulate server requests.
// server.test.ts
import request from 'supertest';
import { app } from './server';
describe('Server', () => {
it('should return a 200 status code for the home page', async () => {
const response = await request(app).get('/');
expect(response.status).toBe(200);
});
});Handling Routes and Dynamic Content
Handling dynamic routes and content in SSR is challenging, as the server must render every possible URL. Angular’s RouterModule and ActivatedRoute provide tools to address this.
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ProductComponent } from './product/product.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'product/:id', component: ProductComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }Ensure the server handles dynamic routes:
// server.ts
app.get('*', (req, res) => {
const { url } = req;
res.render('index', { req, url });
});Asynchronous Data Loading
Asynchronous data loading is common in SSR. Use the ngOnInit lifecycle hook in components to load data, and ensure the server waits for data to resolve before rendering.
// product.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProductService } from './product.service';
@Component({
selector: 'app-product',
template: `
<h1>{{ product.title }}</h1>
<p>{{ product.description }}</p>
`,
})
export class ProductComponent implements OnInit {
product: any;
constructor(private route: ActivatedRoute, private productService: ProductService) { }
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
this.productService.getProduct(id).subscribe(product => {
this.product = product;
});
}
}Ensure the server waits for data loading:
// server.ts
app.get('*', (req, res) => {
const { url } = req;
res.render('index', { req, url }, (err, html) => {
if (err) {
console.error('An error occurred:', err);
return res.status(500).send('Internal Server Error');
}
res.send(html);
});
});SEO and Meta Tags
SSR is SEO-friendly, generating complete HTML pages with meta tags. Dynamically generate meta tags in components to optimize search engine rankings.
// product.component.ts
import { Component, OnInit } from '@angular/core';
import { Meta } from '@angular/platform-browser';
@Component({
selector: 'app-product',
template: `
<h1>{{ product.title }}</h1>
<p>{{ product.description }}</p>
`,
})
export class ProductComponent implements OnInit {
product: any;
constructor(private meta: Meta) { }
ngOnInit() {
this.meta.updateTag({ name: 'description', content: this.product.description });
}
}Multi-Language Support
In SSR, multi-language support can be implemented by detecting user language preferences and rendering translations server-side. The @ngx-translate/core module simplifies this process.
npm install @ngx-translate/core @ngx-translate/http-loader// app.module.ts
import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http);
}
@NgModule({
imports: [
HttpClientModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
})
],
/* other configurations */
})
export class AppModule { }Deploying SSR Applications
Deploying SSR applications involves uploading the built client and server code to a server and ensuring the server runs correctly. Configure a reverse proxy (e.g., Nginx or Apache) to handle SSL and load balancing.
Client-Side Rendering
Client-Side Rendering (CSR) is the most common rendering mode for modern SPAs. In CSR, the application’s logic and view rendering occur in the user’s browser, providing high interactivity and dynamic behavior.
Angular CSR Principles
In CSR, when a user visits the application, the browser downloads the initial HTML, CSS, and JavaScript files. Once loaded, Angular takes over page rendering and event handling. Page navigation, data updates, and UI animations occur client-side without reloading the entire page.
Creating an Angular CSR Application
Angular CLI creates a CSR application by default.
ng new my-client-app
cd my-client-appAngular Components and Templates
Components are Angular’s core building blocks, encapsulating views (HTML templates), logic (TypeScript classes), and styles.
// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'my-client-app';
}<!-- app.component.html -->
<h1>Welcome to {{ title }}!</h1>
<button (click)="changeTitle()">Change Title</button>// app.component.ts
export class AppComponent {
title = 'my-client-app';
changeTitle() {
this.title = 'New Title';
}
}Data Binding and Directives
Angular offers data binding and directives like interpolation ({{ }}), property binding ([] or [attr.]), event binding (() or (event)), and structural directives (*ngIf, *ngFor).
<!-- app.component.html -->
<div *ngIf="isVisible">Visible when true</div>
<input [(ngModel)]="inputValue" placeholder="Type something...">Dependency Injection and Services
Dependency injection, a core Angular feature, enables declarative management of dependencies between components.
// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; // For [(ngModel)]
import { AppComponent } from './app.component';
import { DataService } from './data.service';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [DataService],
bootstrap: [AppComponent]
})
export class AppModule { }// data.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor() { }
getData() {
return ['item1', 'item2', 'item3'];
}
}Routing
Angular’s router enables defining the application’s navigation structure and page links.
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }State Management
For complex state management, use NgRx or RxJS.
// store/state.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
export interface AppState {
counter: number;
}
export const selectAppState = createFeatureSelector<AppState>('app');
export const selectCounter = createSelector(
selectAppState,
state => state.counter
);Unit Testing
Angular CLI integrates Jasmine and Karma for writing and running unit tests.
ng testPerformance Optimization
- Lazy Loading: Load modules and components on demand to reduce initial load time.
- Code Splitting: Webpack and Angular CLI support code splitting to minimize bundle sizes.
- Production Builds: Use the
--prodflag for production builds to enable AOT compilation and code minification.
ng build --prodDeployment
Deploy CSR applications by uploading built static files to a CDN or static file server.
ng build --prod --base-href /my-client-app/Real-World Case Study: Building a CSR E-Commerce Application
Suppose you’re building an e-commerce application with product listings, product details, and a shopping cart.
- Create Angular Project: Use Angular CLI to create the project.
- Design Components: Define components for product lists, product details, and the shopping cart.
- Data Models and Services: Create models and services to fetch product data and manage the cart.
- Routing Configuration: Define routes for navigation between product lists and details.
- State Management: Use NgRx or RxJS to manage cart state.
- Unit Testing: Write tests to ensure component and service correctness.
- Performance Optimization: Implement lazy loading and code splitting, optimize build settings.
- Deployment: Deploy the app to a static file server or CDN.
Summary
Angular’s Client-Side Rendering (CSR) offers a highly dynamic and interactive user experience. By mastering Angular’s components, data binding, dependency injection, routing, and state management, you can build feature-rich, high-performance modern web applications. As frontend technologies evolve, Angular’s CSR mode will remain pivotal for complex SPAs.
Prerendering
Prerendering is a technique that generates static HTML files at build time, combining the benefits of Server-Side Rendering (SSR) and Client-Side Rendering (CSR). Angular’s prerendering generates static HTML pages during the build process, improving First Contentful Paint (FCP) and Search Engine Optimization (SEO).
Angular Prerendering Principles
Prerendering creates static HTML files at build time, served immediately upon user request without dynamic server-side generation. This allows users to see content faster, and search engines can index complete HTML content easily.
Installing Prerendering Tools
The Angular community commonly uses angular-universal and prerender-spa-plugin for prerendering. This guide uses prerender-spa-plugin for its lightweight and easy configuration.
npm install --save-dev @prerenderer/prerender-spa-plugin @prerenderer/slim-dom @prerenderer/plugin-puppeteerConfiguring Prerendering
Configure the prerendering plugin in angular.json. Ensure the production build configuration is set.
// angular.json
{
"projects": {
"my-app": {
"architect": {
"build": {
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputPath": "dist/my-app",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
}
}
}
}
}Add prerendering configuration under architect.build.configurations.production:
// angular.json
{
"projects": {
"my-app": {
"architect": {
"build": {
"configurations": {
"production": {
// ...
"prerender": {
"prerenderer": "@prerenderer/prerender-spa-plugin",
"routes": [
"/",
"/about",
"/contact"
]
}
}
}
}
}
}
}
}Using Puppeteer for Prerendering
The prerender-spa-plugin integrates with Puppeteer, a Node library for controlling headless Chrome or Chromium browsers, allowing the plugin to render your app like a real user.
// angular.json
{
"projects": {
"my-app": {
"architect": {
"build": {
"configurations": {
"production": {
// ...
"prerender": {
"prerenderer": "@prerenderer/prerender-spa-plugin",
"routes": [
"/",
"/about",
"/contact"
],
"plugins": [
["@prerenderer/plugin-puppeteer", {
"launch": {
"args": ["--no-sandbox"]
}
}]
]
}
}
}
}
}
}
}
}Running the Prerendering Build
Running ng build --prod generates static HTML files for the specified routes.
ng build --prodServing the Prerendered Application
Prerendered applications can be deployed like any static website, using Nginx, Apache, or any static file server.
server {
listen 80;
server_name example.com;
root /path/to/dist/my-app;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}Real-World Case Study: Building a Prerendered News Website
Suppose you’re building a news website with multiple categories and article detail pages, aiming for fast loading and SEO-friendliness.
- Create Angular Project: Use Angular CLI to create the project.
- Design Components and Routes: Define components for news lists and article details, configure routes.
- Install Prerendering Tools: Install
prerender-spa-pluginand@prerenderer/plugin-puppeteer. - Configure Prerendering: Set up the prerendering plugin in
angular.json. - Run Prerendering Build: Use
ng build --prodto generate static HTML files. - Deploy Application: Deploy the prerendered app to a static file server.
Summary
Angular prerendering is a powerful technique combining SSR and CSR advantages, offering fast FCP and excellent SEO. Using prerender-spa-plugin and Puppeteer, you can generate static HTML files at build time, enhancing performance and search engine rankings. As web technologies advance, prerendering will remain a key component for building high-performance, SEO-friendly modern web applications.



