Skip to content

SSR Finally a First-Class Citizen in Angular?

SSR Finally a First-Class Citizen in Angular?

I am sure you've already heard about server-side rendering (SSR). Generally, it's a technique used to deliver the HTML content generated by the server to the client. The client-side JavaScript framework then hydrates that HTML content to provide a fully interactive application. SSR provides numerous benefits if used correctly, including improved initial load times, better content indexing by search engines, and improved performance on lower-tier devices.

Driven by the need for faster, more efficient web applications, with the rise in popularity of Node.js and, thus, server-side JavaScript, SSR has become a de facto standard in modern web development frameworks. At first, SSR was being provided as a part of meta-frameworks like Next.js, Nuxt, or Analog.js. Then, React introduced React Server Components, which marked a significant milestone in the way we think about front-end development. We think of front-end frameworks as being effectively full-stack, and a purely SPA approach is falling out of favor.

However, I'm willing to bet that if you're a developer focused mostly on Angular, you may not have heard as much about SSR as your React counterparts. For a long time, Angular has been mostly focused on the SPA approach, and while we had Angular Universal or Analog.js, it was not as widely adopted. And that makes sense, given that Angular was widely regarded as best suited for complex, interactive applications, such as SaaS products or internal company tools, where SEO and initial load times were not as critical as they are for consumer-facing applications.

Nevertheless, we have been blessed with the so-called Angular Renaissance, which has, among other things, brought SSR to the forefront of Angular development with the @angular/ssr package. Let's look at what Angular SSR is, how it works, and how you can get started.

@angular/ssr

The @angular/ssr package is not new; it was written from scratch. It's still Angular Universal. However, it's been pulled into the Angular CLI repository, and the appropriate schematics have been added to make it easier to use. What that means is that you can now add SSR to your Angular application with a single command:

ng add @angular/ssr

and you can generate a new SSR application with:

ng new my-ssr-app --ssr

And to make SSR a first-class citizen, the Angular team is planning to make it the default in the next version.

But surely, it's not only rebranding and some CLI commands, you may say. And you're right. You may remember that hydration in Angular Universal has not been optimal as it was destructive and required essentially a complete re-render of the application, thus pretty much negating the benefits of SSR. With the introduction of non-destructive hydration in Angular 16 and its stability in Angular 17, the Angular team has taken a significant step forward in making SSR a first-class citizen.

How to Make an SSR-capable Angular Application

Let's look at how to use Angular SSR in practice by creating a simple SSR app. First, we need to create a new Angular application with SSR support:

ng new my-ssr-app --ssr

That will generate a few server-specific files:

  • server.ts, which is an Express server configured to handle Angular SSR requests by default but can also be extended to handle API routes, or anything else that Express can handle.
  • src/app/app.config.server.ts provides configuration for the server application similar to your app.config.ts, which defines the configuration for your client application. Both export an instance of ApplicationConfig
  • src/main.server.ts bootstraps the server application in a similar way to what main.ts does for your client app by calling the bootstrapApplication function.

You should have an SSR application ready just like that. You can verify that by running ng serve. The terminal output should show you information about the server bundles generated, and you should be able to see the default app when going to http://localhost:4200.

Screenshot from terminal with stats about generated server and browser bundles.

Open the Developer Tools and check the console to see that hydration is enabled. You'll see a message with hydration-related stats, such as the number of components and nodes hydrated.

Screenshot from development console saying that Angular hydrated 1 component and 66 nodes.

Speaking of hydration...

Client Hydration

If you open the src/app/app.config.ts in your new SSR application, you'll notice one interesting provider: provideClientHydration(). This provider is crucial for enabling hydration - a process that restores the server-side rendered application on the client.

Hydration improves application performance by avoiding extra work to re-create DOM nodes. Angular tries to match existing DOM elements to the application's structure at runtime and reuses DOM nodes when possible. This results in a performance improvement measured using Core Web Vitals statistics, such as reducing the Interaction To Next Paint (INP), Largest Contentful Paint (LCP), and Cumulative Layout Shift (CLS). Improving these metrics also positively impacts SEO performance.

Without hydration enabled, server-side rendered Angular applications will destroy and re-render the application's DOM, which may result in visible UI flicker, negatively impact Core Web Vitals like LCP, and cause layout shifts. Enabling hydration allows the existing DOM to be reused and prevents flickering.

Event Replay and Improved Debugging

With Angular 18, event replay, a feature that significantly enhances the SSR experience, came into developer preview. Event replay captures user interactions during the SSR phase and replays them once the application is hydrated. This ensures that user actions, such as adding items to a cart, are not lost during the hydration process, providing a seamless user experience even on slow networks.

To enable event replay, you can add withEventReplay to your provideClientHydration call:

bootstrapApplication(App, {
 providers: [provideClientHydration(withEventReplay())],
});

Furthermore, Angular 18 takes another step towards making SSR a first-class citizen by enhancing the debugging experience with Angular DevTools. Now, DevTools include overlays and detailed error breakdowns, empowering developers to visualize and troubleshoot hydration issues directly in the browser.

Constraints

However, hydration imposes a few constraints on your application that are not present without hydration enabled. Your application must have the same generated DOM structure on both the server and the client. The hydration process expects the DOM tree to have the same structure in both environments, including whitespaces and comment nodes produced during server-side rendering.

Direct DOM Manipulation

The fact that server and client DOM structures must match means that components that manipulate the DOM using native DOM APIs or use innerHTML or outerHTML will encounter hydration errors. The errors are thrown because Angular is unaware of these changes and cannot resolve them during hydration. Examples include accessing the document, querying for specific elements, and injecting additional nodes using appendChild. Detaching DOM nodes and moving them to other locations will also cause errors.

Direct DOM manipulation is generally considered bad practice in Angular, so I recommend refactoring your components to avoid it regardless of hydration. However, in warranted cases where refactoring is not feasible, you can use the ngSkipHydration attribute until a hydration-friendly solution can be implemented.

You can use it directly in a template:

<app-example ngSkipHydration></app-example>

or as a host binding:

@Component({
  ...
  host: {ngSkipHydration: 'true'},
})
class ExampleComponent {}

Note: The ngSkipHydration attribute forces Angular to skip hydrating the entire component and its children. So use this attribute carefully, as, for example, applying it to your root application component would effectively disable hydration for the entire application.

Valid HTML Structure

Another constraint is that invalid HTML structures in component templates can result in DOM mismatch errors during hydration. Common issues include:

  • <table> without a <tbody>
  • <div> inside a <p>
  • <a> inside an <h1>
  • <a> inside another <a>

Again, if you have an invalid HTML, you should fix this regardless of hydration. If you are unsure about your HTML's validity, you can use the W3 validator to check it.

Zoneless apps or custom Zone.js

Hydration relies on a signal from Zone.js when it becomes stable. Custom or "noop" Zone.js implementations may thus disrupt the timing of this event, triggering the serialization or cleanup process too early or too late. This configuration is not yet fully supported.

While Zoneless is already available in developer preview and we could assume this issue will be resolved when it lands as stable, there is no explicit ETA mentioned in the official roadmap.

I18n

One of the biggest blockers for adoption of client hydration was the lack of support for internationalization with hydration. By default, Angular 17 skipped hydration for components that used i18n blocks, re-rendering them from scratch. In Angular 18, however, hydration for i18n blocks got into developer preview.

To enable hydration for i18n blocks, you can add withI18nSupport to your provideClientHydration call.

import {
  bootstrapApplication,
  provideClientHydration,
  withI18nSupport,
} from '@angular/platform-browser';

bootstrapApplication(AppComponent, {
  providers: [provideClientHydration(withI18nSupport())]
});

Third-Party Libraries with DOM Manipulation

The last thing to consider is third-party libraries that manipulate DOM, such as D3 charts. They could cause DOM mismatch errors when hydration is enabled. You can use the ngSkipHydration attribute to skip hydration for components rendered using these libraries.

Conclusion

The Angular team has been doing great work to make the framework more modern and competitive, hearing the community feedback and adjusting the roadmap accordingly. And SSR is an excellent example of that. With the introduction of non-destructive hydration in Angular 16 and making it stable in Angular 17, the Angular team has taken a significant step forward in making SSR a first-class citizen in Angular. The @angular/ssr package is now part of the Angular CLI, making adding SSR to your Angular application easier. The Angular team plans to make SSR the default in the next version, further solidifying its position in the Angular ecosystem.

However, as with any new feature, there are some constraints and best practices to follow when using SSR with Angular. You should be aware of hydration's limitations and ensure your application is compatible with it. However, if you follow the guidelines provided by the Angular team, you should be able to take advantage of the benefits of SSR without any significant issues.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.