The State of Server Side Rendering (SSR) in Angular

Angular has supported server side rendering in various forms since the first release of 2.0. But having tried many of these solutions, I can assure you that they we're not perfect and didn't see very much adoption beyond some scaled enterprise use cases.

This has undergone a complete reversal as of 2025 and I'm now using it in basically every angular project that I build.

But why?

SSR doesn’t fundamentally change your user experience. If you do it right, a loaded application should look and function identically before and after your use of SSR.

There are two main reasons that this is tech you should consider today.

  1. Robots - The Google crawler can understand SPAs and modern web applications pretty well, so your content will be indexed and available to the world, but Google seems to be the exception.

    Most social media sites like BlueSky, productivity and communication tools like Slack, Discord, Notion, etc all want to crawl any links mentioned and render helpful previews of the linked content. Virtually none of these tools are capable of rendering JavaScript applications, so sending down fully rendered HTML is critical.

  2. Humans - While SSR is required for the robots, humans matter too. At the end of the day if the software you are building doesn’t serve humans, then it’s probably not adding much value.

    Using SSR an an Angular app can improve page performance, and help your users do what they want to do faster, resulting in happier more successful users.

Concepts

To understand the state and complexity of SSR, you need to understand the various approaches and tools at your disposal.

  1. Server Side Rendering - While considered the main topic, this is actually specific strategy that involves every request from a user being executed fully on the server, resulting in HTML that can be sent to the browser and rendered quickly.
  2. Prerendering - Similar to Service Side Rendering, prerendering (also known as Static Site Generation or SSG) is a build time step that creates a permanent version of the static HTML content as part of the build.
  3. Client Side Rendering (CSR) - This is the traditional SPA approach where you’re not worried about sending down HTML, but instead JavaScript code that can be executed entirely in the browser to give the user a great experience.

It’s important to think about these three because in modern Angular (v20+) you can actually mix and match these strategies for different routes of your application.

Getting Started

To actually get started, just use the modern CLI. It will prompt you if you want to add SSR or prerendering, and you should say Yes!

If you want to specify it manually, you can do that too.

ng new my-project --ssr

Or you can alternatively add it to an existing project

ng add @angular/ssr

Once you have done this, a bunch server-specific files will be added to your project, and your builds will result in an actual JavaScript application (server.mjs), rather than just static files.

One of the most important of these is your app.routes.server.ts file which configures the SSR strategy for the routes of your application. You can think of it like a collorary to your normal app.routes file.

import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  { path: '**', renderMode: RenderMode.Prerender },
  { path: 'auth', renderMode: RenderMode.Client },
  { path: 'rich-data', renderMode: RenderMode.Server },
];

One challenge with a configuration like this is that while the server knows all of the parameters at request time when a request comes in for a user, the application doesn’t know all of the parameters to prerender ahead of time. Fortunately there’s a method that lets you enumerate all of the paths to prerender at build time.

{
   path: 'post/:id',
   renderMode: RenderMode.Prerender,
   async getPrerenderParams() {
     const dataService = inject(PostService);
     const ids = await dataService.getIds();
     return ids.map(id => ({ id }));
}

In this example, you can imagine a blog site where at build-time we fetch the list of posts from a backend, and prerender the content for all of the pages that exist today.

Builds

Now that your application is setup and configured with SSR, it’s important to understand the different output of ng build. Instead of static HTML, JS, and CSS, you’re actually creating a server capable of listening on a port and serving files.

Angular builds create a web server

Your server.mjs is a server that can be run with node, bun, deno or whatever your preferred JS execution environment is. If the file requested exists in the browser folder, it will be served straight to the user. If not, it will figure out based on your configuration that’s stored in a manifest how to respond, such as by performing a full SSR of the application.

TransferState and httpResource Signals

I’m a huge fan of httpResource and signals, but there’s a gotcha to be aware of.

By default any data in the component loaded from the web as part of a SSR request is going to be encoded in the HTML payload as JSON, so that when loading on the client side, the transferred state of the cache is used instead of making another web request.

This seems like an intelligent strategy, but it can backfire pretty easily. Imagine you have 10MB of data coming back from an API that you are rendering in a long list or large table. What you’ll actully be sending to the browser is two copies of that data. One as the HTML you have rendered, and another as the JSON representation to assist with the hydration of that component.

The more performant approach is actually to send the HTML down without the transferred state and then remake the API request in a non-blocking way.

You can accomplish this globally by just turning off the transfer cache.

withNoHttpTransferCache()

Hydration

Hydration is an important but invisible part of modern SSR. When the browser receives HTML, it doesn’t know how to wire up buttons, events, etc as it’s just the HTML. Historically when you used SSR with Angular, Angular would just delete all of the DOM and replace it with newly rendered elements, but in 2025 we have hydration. This means that instead of deleting the elements, potentially causing a large contentful paint and affecting performance metrics, we wire up all of the behaviors and functionality into the existing DOM.

You can opt out of hydration for any component, which you must do if your application ever manually manipulates DOM or if you have imperfect DOM that doesn’t match what the browser expects (eg. <table> tags often implicitly create a <thead> or <tbody> for you).

 selector: 'app-unhydrated',
 host: { ngSkipHydration: 'true',
},

Not only is hydration powerful, Angular has a super power not found elsewhere. Angular can do incremental hydration, meaning you can use fine grained control to hydrate a single component at a time based on conditions and triggers that you choose.

Turn it on in your providers.

export const appConfig: ApplicationConfig = {
 providers: [
   provideBrowserGlobalErrorListeners(),
   provideZonelessChangeDetection(),
   provideRouter(routes),
   provideClientHydration(withEventReplay(),
      withIncrementalHydration()),
 ],};

With incremental hydration you can replace lazy component loading with lazy component hydration.

@defer (on hover) {
   <app-deferred />
} @placeholder {
   <div>Component placeholder</div>
} 
@defer(hydrate on hover) {
   <app-deferred />
}

If we imagine our deferred component does some heavy lifting or computations, we get all of the benefits of not having to rely on a placeholder, but deferring the actual hydration until a user wants to interact with it.

Conclusion

Hopefully this has been a helpful welcome to the world of SSR as of Angular v20/v21 in 2025. Let me know if you have any cool tricks or tips that have made you successful with SSR.

You can check out a demo of these concepts at ngconf.fluin.io or check out the source code on GitHub.