In the modern web development landscape, performance and user experience are paramount. One of the most effective ways to achieve this is through Server-Side Rendering (SSR). Angular, with its robust framework, offers SSR capabilities via Angular Universal. However, a crucial part of optimizing SSR is hydration, which bridges the gap between server-rendered HTML and client-side Angular applications. In this blog, we’ll dive deep into Angular hydration with SSR, exploring its benefits, setup, and implementation.
If you wants to know about the Top Angular Libraries and Tools in 2024, you can refer here.
What is Server-Side Rendering (SSR)?
Server-side rendering (SSR) is the process of rendering web pages on the server instead of in the browser. This approach improves performance, especially for the initial load, by delivering fully rendered HTML to the client, reducing the time it takes for users to see content.
Benefits of SSR:
- Improved Performance: Faster initial load times.
- SEO Optimization: Better indexing by search engines.
- Enhanced User Experience: Quick content delivery.
What is Hydration?
Hydration is the process of converting the server-rendered HTML into a fully interactive client-side application. When the Angular framework takes over the static HTML, it “hydrates” it by attaching event listeners, initializing components, and bringing the application to life.
Benefits of Hydration:
- Smooth Transition: Ensures a seamless transition from static HTML to an interactive application.
- Reduced Time to Interactive (TTI): Faster TTI improves user engagement.
- Optimized Rendering: Leverages server-side rendering while benefiting from client-side interactivity.
Setting Up Angular SSR
Before diving into hydration, let’s set up Angular SSR using Angular Universal.
Step 1: Create a New Angular Application
If you don’t have an existing Angular application, create one using Angular CLI:
ng new angular-ssr-example
cd angular-ssr-example
Step 2: Add Angular Universal
Add Angular Universal to your project:
ng add @nguniversal/express-engine
This command sets up Angular Universal with an Express server, generating the necessary files and configurations.
Step 3: Update Server-Side Code
Navigate to the server.ts file to ensure it handles server-side rendering correctly:
import 'zone.js/dist/zone-node';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';
import { AppServerModule } from './src/main.server';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/angular-ssr-example/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set('view engine', 'html');
server.set('views', distFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});
return server;
}
function run(): void {
const port = process.env.PORT || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export * from './src/main.server';
Step 4: Build and Serve
Build your application for SSR:
npm run build:ssr
Serve your application:
npm run serve:ssr
Your Angular application is now set up for SSR!
Implementing Hydration
With SSR set up, hydration is the next critical step to ensure that your server-rendered application becomes fully interactive on the client side.
Step 1: Enable Zone.js
Ensure zone.js is enabled in your Angular application, as it’s crucial for change detection. Typically, this is already included in Angular projects.
import 'zone.js/dist/zone-node';
Step 2: Optimize AppModule
Update AppModule to support both server and client environments:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'angular-ssr-example' }),
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Step 3: Handle Client-Side Bootstrapping
Update main.ts to handle client-side bootstrapping:
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
document.addEventListener('DOMContentLoaded', () => {
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
});
Step 4: Use Transfer State for Improved Performance
To enhance hydration performance, use Angular’s Transfer State feature to transfer data from the server to the client, avoiding duplicate HTTP requests.
Server-Side Transfer State
In your server-side code, add data to the transfer state:
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { Injectable, Inject } from '@angular/core';
const STATE_KEY = makeStateKey<any>('stateKey');
@Injectable()
export class StateService {
constructor(private transferState: TransferState, @Inject(REQUEST) private req: any) {
if (this.req) {
this.transferState.set(STATE_KEY, { data: 'Server-side data' });
}
}
}
Client-Side Transfer State
In your client-side code, retrieve the data from the transfer state:
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { Injectable } from '@angular/core';
const STATE_KEY = makeStateKey<any>('stateKey');
@Injectable()
export class StateService {
constructor(private transferState: TransferState) {
const state = this.transferState.get(STATE_KEY, null);
if (state) {
console.log('Transferred State:', state);
}
}
}
Conclusion
Hydration is a crucial aspect of optimizing Angular applications with SSR. By setting up Angular Universal and implementing hydration, you can significantly improve the performance and user experience of your application. The combination of server-side rendering and client-side hydration ensures that your application is both fast and interactive, providing the best of both worlds.
Stay tuned for more advanced techniques and tips on optimizing your Angular applications. Happy coding!
Finally, for more such posts, please follow our LinkedIn page- FrontEnd Competency.