Server-Side Rendering (SSR) and Hydration in Angular

Vikesh Mittal
9 min readSep 7, 2024

--

What is the concept of Server-Side Rendering?

Server-Side Rendering (SSR) in web development refers to the process of rendering a web page’s HTML on the server instead of in the browser. The server pre-generates the HTML for the requested page and sends it to the browser, which displays the content immediately without requiring the client to execute JavaScript to load the page’s content. SSR is commonly used to improve both performance and SEO for dynamic web applications.

How SSR Works:

  1. Request to the Server: When a user makes a request to a website, the server receives the request.
  2. Server-Side Rendering: The server runs the application (often a JavaScript framework like React, Angular, or Vue) to generate a fully-formed HTML page. This page includes the complete structure and content that the user would see.
  3. Sending HTML to Client: The pre-rendered HTML is sent to the client (browser), which displays the page almost instantly.
  4. Client-Side Hydration: Once the pre-rendered HTML is displayed, the client downloads the necessary JavaScript files, re-attaches event listeners, and enables dynamic features like user interactions. This process is called hydration.

Server-Side Rendering in Angular

Angular adopted Server-Side Rendering (SSR) in 2017 with the introduction of Angular Universal in version 4. This decision was driven by several key factors, primarily the need to enhance performance and improve search engine optimization (SEO) for Angular applications, which were heavily client-rendered.

After its introduction, Angular Universal continued to evolve. Subsequent versions focused on simplifying the integration with Express.js servers, improving the build process, and adding support for popular caching mechanisms to further improve the performance of SSR-rendered Angular apps.

Photo by Merakist

Why Angular adopted SSR?

  1. SEO Limitations: Angular applications prior to SSR relied on client-side rendering, where the browser executed JavaScript to load and display content. However, search engine crawlers like Google’s, at the time, struggled with fully indexing JavaScript-rendered content, affecting the discoverability of Angular apps. SSR pre-renders the app on the server, allowing search engines to crawl a fully-formed HTML document.
  2. Performance: Large-scale Angular apps often faced performance issues, especially on slower networks or devices. Client-side rendering required loading all JavaScript and executing it before showing the content. SSR helps by rendering the HTML on the server, reducing the initial load time and allowing users to see content faster.
  3. Faster Time to Interactive (TTI): By rendering the HTML on the server, Angular Universal enables users to see the page immediately after it loads, while the JavaScript bootstraps in the background to handle interactivity. This is particularly beneficial for improving the perceived performance of Angular applications.

By introducing SSR, Angular was able to compete with other frameworks like React (with Next.js) and Vue (with Nuxt.js), which also offered SSR to optimize the user experience and improve web visibility.

What is Hydration?

After SSR has sent the pre-rendered HTML to the client, the hydration process kicks in. Hydration is the process by which the client-side JavaScript takes over the static HTML and makes it interactive by re-attaching event listeners, loading the Angular framework, and initializing the app state.

Without hydration, the rendered HTML would remain static and non-interactive, just like any basic HTML page.

Hydration ensures that:

  1. Client-side JavaScript takes control of the app.
  2. The client app restores the exact state from the server-rendered page, preventing any flickering or reloading of content.
  3. The framework doesn’t need to completely rebuild the DOM. Instead, it attaches to the existing pre-rendered DOM elements, making the process much faster.

Types of Hydration

  1. Full Hydration: The complete Angular application is loaded and bootstrapped in the browser, taking over the server-rendered HTML entirely.
  2. Partial Hydration: Instead of rehydrating the entire app, only specific parts of the page are rehydrated. This is more efficient when dealing with large applications where only portions of the app need to be interactive immediately.
  3. Progressive Hydration: The hydration process occurs gradually as the user interacts with different parts of the application. It provides faster initial interactivity by only hydrating parts of the app the user interacts with, loading other parts on demand.

Implementing SSR and Hydration in Angular

Here I assume that you already have working knowledge of Angular. I will provide the basic setup instructions for SSR in Angular and you would need to update this based on your use case.

Setting up Angular Universal

To enable SSR in your Angular application, you need to install and configure Angular Universal. This can be done using the Angular CLI with the following command:

ng add @nguniversal/express-engine

Configuring Server-Side Code

Adding Angular Universal will modify your angular.json, and the project will contain a server-side entry point server.ts where you can manage the Express.js server configuration for rendering the Angular app.

import 'zone.js/dist/zone-node';
import * as express from 'express';
import { join } from 'path';
import { ngExpressEngine } from '@nguniversal/express-engine';
import { AppServerModule } from './src/main.server';

// create express app and assign a port as you need
const app = express();
const PORT = process.env.PORT || 4000;

// bootstrap the app using the server module which will process
// incoming requests
app.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
app.set('view engine', 'html');
app.set('views', join(process.cwd(), 'dist/browser'));

// Serve static files
app.get('*.*', express.static(join(process.cwd(), 'dist/browser')));

// Render the app server-side
app.get('*', (req, res) => {
res.render('index', { req });
});

// start the server
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

Example of how Angular would do hydration

Imagine a product page that SSR has already rendered on the server. When the page loads in the browser, Angular re-initializes the components, attaches events to buttons, and restores the application’s state.

<!-- Server-side pre-rendered content -->
<div>
<h1>Product Name: Angular T-Shirt</h1>
<button>Add to Cart</button>
</div>

<!-- Client-side Hydration process -->
<!-- Angular takes control of the button -->
<script>
const addToCartButton = document.querySelector('button');
addToCartButton.addEventListener('click', () => {
console.log('Product added to cart!');
});
</script>

Step-by-Step SSR with Hydration:

  1. Request: A user visits example.com/product/1, so server receives the request.
  2. Server Rendering: The Angular Universal server pre-renders the product page with full HTML content.
  3. Sending to Client: The HTML is sent to the client, which instantly displays the product details without waiting for JavaScript to load.
  4. Hydration: Once the Angular app is bootstrapped on the client, it reattaches the necessary JavaScript (like event listeners) to the static HTML, making it interactive.
  5. Interactivity Restored: The user can now interact with the page (e.g., add the product to a cart, navigate, etc.).

Reusing SSR-Generated HTML Across Multiple Servers

In large-scale deployments, running SSR on every request can be computationally expensive. One way to improve performance and scalability is by caching the SSR-generated HTML and reusing it across multiple servers. This is especially useful in multi-server environments and cloud-based deployments where SSR rendering costs can be high.

Step 1: Store SSR-Generated HTML

Once SSR is executed, the generated HTML can be stored in a caching layer like Redis, Memcached, or even a file-based cache on the server. This cache can be shared across multiple Node.js servers, reducing the need to re-render the same page for every request.

  1. Set up Redis: First, install Redis and the redis npm package.
  2. Modify server.ts to Cache SSR HTML
// Note that this is delta code on top of previous example

/**
* In this example, Redis is used to store and serve SSR-generated HTML.
* On each request, the server checks the cache for an existing pre-rendered
* page and serves it if available. If the cache is empty, SSR generates
* the HTML and stores it in Redis for future requests.
*/
import * as redis from 'redis';

const redisClient = redis.createClient();
const cacheExpiry = 60 * 60; // 1 hour

// Redis middleware to check cache
app.get('*', (req, res, next) => {
const urlKey = req.url;
redisClient.get(urlKey, (err, data) => {
if (err) throw err;
if (data) {
return res.send(data); // Serve cached HTML
} else {
next(); // Proceed to SSR if cache is empty
}
});
});

// SSR rendering and storing in Redis cache
app.get('*', (req, res) => {
res.render('index', { req }, (err, html) => {
if (err) throw err;
redisClient.setex(req.url, cacheExpiry, html); // Cache the HTML
res.send(html);
});
});

Step 2: Share Cached SSR HTML Across Multiple Node Servers

Once the HTML is cached in a shared system like Redis, multiple Node.js servers in a load-balanced environment can access and reuse the same cached content. This approach is useful in environments like Kubernetes, where several instances of the same app might be running across different servers.

  • Kubernetes: Deploy a Redis service in your Kubernetes cluster and configure all Node.js servers (pods) to connect to this Redis instance for retrieving and caching SSR HTML.
  • AWS ElastiCache: If you are using AWS, ElastiCache can act as a centralized caching layer for your Node.js servers, ensuring that the SSR-generated HTML is shared across your AWS instances.

Step 3: What to do with New Deployments?

When a new version of the Angular app is deployed, the cache might contain outdated HTML. Invalidate the cache whenever there is a new deployment, forcing the servers to generate fresh SSR HTML.

// Note that this is delta code on top of previous example

const appVersion = 'v1.2.3'; // Update this with each deployment
const urlKey = `${appVersion}_${req.url}`; // it will force new url key

redisClient.get(urlKey, (err, data) => {
if (data) {
res.send(data);
} else {
// Render and store new HTML
}
});

Internal Working of Angular Server-Side Rendering (SSR)

If you are still engaged with this article,made it till here and still not bored then I will try to explain a bit more aout internal working of SSR in Angular.

Angular Universal and its Core Components

Angular Universal enables SSR by extending Angular’s platform to support server-side execution. Its architecture includes:

  1. @nguniversal/express-engine: This is the core package that integrates Angular with the Express.js server to enable SSR. It acts as a renderer for Angular components on the server.
  2. @angular/platform-server: This package provides APIs that help Angular run in a server-side environment. It handles the pre-bootstrapping of the app on the server and helps generate the static HTML.
  3. Zone.js: Zone.js is a key library that enables change detection in Angular apps. During SSR, Angular needs to handle asynchronous operations like HTTP requests on the server, and Zone.js tracks these operations to know when to complete rendering.

Rendering Flow in Angular SSR

  1. Express Server: The request is received by the Express server, which uses @nguniversal/express-engine to handle Angular-specific rendering. The Express server passes the incoming request to Angular’s server-side engine.
  2. App Module Transfer: The Angular Universal engine is bootstrapped with the same Angular app module as used in the client-side, except for some minor modifications (like importing AppServerModule instead of AppModule). This allows the same app to be run on both the client and the server.
  3. The AppServerModule is pre-configured to execute the Angular app in a server context using @angular/platform-server. Angular pre-renders the application on the server just like it would on the client side. This process involves 2 things. First Angular runs the code for the current route and resolves any data (like API calls or route data) before rendering the page. Once the data is fetched, Angular’s rendering engine converts the component tree into HTML. During this step, Angular traverses the entire component hierarchy, executing the components’ lifecycle methods (ngOnInit etc.) as if they were running on the client.
  4. Zone.js and asynchronous tasks: Angular uses Zone.js to track asynchronous tasks (such as HTTP requests) to ensure that rendering doesn’t complete until all pending operations are finished. Zone.js wraps each async operation and informs Angular when the app is ready for rendering. This ensures that the final HTML sent to the browser contains all necessary content.
  5. Once the rendering is complete, the fully-rendered HTML page is generated by Angular Universal and returned to the Express.js server. The server then sends this HTML back to the browser, allowing the user to see the fully-rendered page immediately.

Handling Routing in SSR

Routing is handled differently during SSR and on the client:

  • Server-Side Routing: When a user requests a page, the server-side router checks the current route, fetches any required data, and renders the HTML.
  • Client-Side Routing: Once the app is hydrated, Angular’s client-side router takes over. Subsequent navigation within the app happens entirely on the client side without reloading the page, making transitions faster.

Learning and implementing the SSR might take more than this article but I hope this will help you understand the concept and how Angular processes SSR. Feel free to drop a comment for more discussion on the concept.

--

--

Vikesh Mittal
Vikesh Mittal

Written by Vikesh Mittal

User Interface Architect | Passionate about Micro-frontends | Angular | React | https://vikeshmittal.com/

No responses yet