Today we're talking about a new feature released with Angular version 16, which is Client Hydration. Hydration comes in handy if you have an Angular Universal (Server-side-rendering enabled) application.
Why use Server-Side Rendering?
There are three main reasons to create a Universal version of your application.
- Facilitate web crawlers through search engine optimization (SEO)
- Improve performance on mobile and low-powered devices
- Show the first page quickly with a first-contentful paint (FCP)
Facilitate web crawlers (SEO)
Google, Bing, Facebook, Twitter, and other social media sites rely on web crawlers to index your application content and make that content searchable on the web. These web crawlers might be unable to navigate and index your highly interactive Angular application as a human user could do.
Angular Universal can generate a static version of your application that is easily searchable, linkable, and navigable without JavaScript. Universal also makes a site preview available because each URL returns a fully rendered page.
Improve performance on mobile and low-powered devices
Some devices don't support JavaScript or execute JavaScript so poorly that the user experience is unacceptable. For these cases, you might require a server-rendered, no-JavaScript version of the application. This version, however limited, might be the only practical alternative for people who otherwise couldn't use the application at all.
Show the first page quickly
Displaying the first page quickly can be critical for user engagement. Pages that load faster perform better, even with changes as small as 100ms. Your application might have to launch faster to engage these users before they decide to do something else.
With Angular Universal, you can generate landing pages for the application that look like the complete application. The pages are pure HTML, and can display even if JavaScript is disabled. The pages don't handle browser events, but they do support navigation through the site using routerLink.
In practice, you'll serve a static version of the landing page to hold the user's attention. At the same time, you'll load the full Angular application behind it. The user perceives near-instant performance from the landing page and gets the full interactive experience after the full application loads.
What is hydration?
Hydration is the process that restores the server side rendered application on the client. This includes things like reusing the server rendered DOM structures, persisting the application state, transferring application data that was retrieved already by the server, and other processes.
Why is hydration important?
Hydration improves application performance by avoiding extra work to re-create DOM nodes. Instead, Angular tries to match existing DOM elements to the applications structure at runtime and reuses DOM nodes when possible.
Before Angular 16: Destructive Hydration
Angular’s destructive hydration is a process that occurs when an Angular application is rendered on the server and then transferred to the browser. The browser then re-renders the application, replacing the existing DOM elements with new ones. This can cause problems such as losing event listeners, breaking animations, and triggering unnecessary change detection cycles.
The above process is called destructive hydration because the client app discards the pre-rendered HTML and reloads the entire page.
The problem of destructive hydration is page flickering, which happens when the server-side-rendered markup is replaced by the client-side rendered content. Multiple issues (i.e., 13446) have been opened in the Angular GitHub repo to resolve this, and it is considered to be a main limitation of the Angular Universal [Read more].
Step 1 - Implementing Server Side Rendering
Before moving forward I recommend you should clone this repository's commit history or feel free to clone the repo.
git clone https://github.com/nishanc/angular-tour-of-heroes.git
Before implementing SSR, let's check current performance of the application using Lighthouse.
Looks really bad right? Assuming you already have an Angular application or cloned this repo, we just need to run the following command to enable server-side rendering:
ng add @nguniversal/express-engine
This command will update your app to add a server-side application for SSR support. Checkout this commit to see all the changes it did, like these new commads it added to the `package.json`.
"dev:ssr": "ng run angular-tour-of-heroes:serve-ssr",
"serve:ssr": "node dist/angular-tour-of-heroes/server/main.js",
"build:ssr": "ng build && ng run angular-tour-of-heroes:server",
"prerender": "ng run angular-tour-of-heroes:prerender"
We can summarize what has happened like this.
Dependency Installation:
The Angular Universal package @nguniversal/express-engine and its necessary dependencies will be installed in your project. These include server-side rendering capabilities, enabling your Angular app to be rendered on the server.
Configuration Updates:
Your project's configuration files will be updated to include server-side rendering configurations. This often includes changes to your angular.json file, where build configurations for server-side rendering are added.
Entry Point Creation:
Angular Universal creates specific entry points for the server-side code. These entry points allow your application to be run on a Node.js server.
Updates to main.ts:
Angular Universal will typically update your main.ts file to include code necessary for bootstrapping your application on the server side.
Initialization for Server-Side Rendering:
Angular Universal will set up the necessary modules and services required for server-side rendering, enabling your Angular application to generate HTML content on the server.
Build Commands:
After the installation and setup, you might have new commands available for building and serving your application with SSR. For instance, you might have a command like npm run build:ssr or npm run serve:ssr added to your project.
Step 2 - Implementing Hydration
To enable non-destructive hydration, we need to import the provideClientHydration function as the provider of AppModule:
providers: [
provideClientHydration()
],
That's it. you have successfully configured Angular SSR with Hydration.
Let's check the performance now.
It's 100% now!. That's a considerable improvement. You might have to do more tweaks to get 100% improvement in your application, but this is a start.
Direct DOM Manipulation
If you have components that manipulate the DOM using native DOM APIs or use innerHTML or outerHTML, the hydration process will encounter errors. Specific cases where DOM manipulation is a problem are situations like accessing the document, querying for specific elements, and injecting additional nodes using appendChild. Detaching DOM nodes and moving them to other locations will also result in errors.
It is best to refactor your component to avoid this sort of DOM manipulation. Try to use Angular APIs to do this work, if you can. If you cannot refactor this behavior, use the ngSkipHydration attribute (described below) until you can refactor into a hydration friendly solution.
How to skip hydration for particular components
Some components may not work properly with hydration enabled due to some of the aforementioned issues, like Direct DOM Manipulation. As a workaround, you can add the ngSkipHydration attribute to a component's tag in order to skip hydrating the entire component.
<app-messages ngSkipHydration></app-messages>
Alternatively you can set ngSkipHydration as a host binding.
@Component({
selector: 'app-messages',
templateUrl: './messages.component.html',
styleUrls: ['./messages.component.css'],
host: {ngSkipHydration: 'true'}
})
The ngSkipHydration attribute will force Angular to skip hydrating the entire component and its children. Using this attribute means that the component will behave as if hydration is not enabled, meaning it will destroy and re-render itself.
Follow me on Twitter (X) to keep in touch with new blog posts. Thank you for reading till the end, I will see you in the next one. 😊🙌
Follow @nishan_cw