Angular Universal Server-side rendering (SSR) how and when to use it
A deep dive into server-side rendering with Angular Universal, what is it, when to use it and how to implement it. These are the topics we will go through in great detail in this blog post. At the end of this article, you will exactly know what Angular Universal is, what it offers, how to optimize it, and when to use it in your applications.
Table of content:
Angular Universal, what is it?
In some scenarios, you might not want this behavior but rather request the data and render the pages on the server before it's shipped to the browser. Angular Universal is the tool given to developers by the Angular framework to enable this functionality. It allows you to generate static pages rendered on the server that gets bootstrapped by our client. In simple terms, this generally means that our applications render more quickly and show the general layout of the website before it becomes fully interactive.
How does SSR with Angular Universal work?
Angular Universal will request the data needed to build the page and render the initial HTML and CSS before it's sent to the browser. Rendering the HTML pages can be done by pre-rendering the HTML page's during build time. This means you have a pre-render step during your application build that will generate the static HTML pages ahead of time and include them in your build output. You can also render the HTML pages server-side on the fly when it's requested by a user. This means you have no pre-rendered pages in your build output, but your server will request data and render the initial HTML and CSS when a user requests the (initial) page.
Angular Universal will not only ship the pre-rendered HTML and CSS to the browser but also a regular client-side Angular application. So that after the pre-rendered HTML is loaded your regular Angular application takes over again. This way the rest of the application is rendered client-side and we can still enjoy the behavior of a single-page application.
But now you might be asking, why use Angular Universal and server-side rendering when only our initial page load is rendered on the server and the rest gets rendered runtime and in the browser like any other Angular application?
Why use server-side rendering and Angular Universal?
Server-side rendering serves a few very specific purposes. If you are building an application where this is not a real factor or the difference is negligible you might consider building your Angular application without server-side rendering. Angular Universal and server-side rendering gives you:
Better performance on mobile and low-powered devices
The first contentful paint is faster with server-side rendering
Server-side rendering improves the SEO of your application
Better performance on mobile and low-powered devices:
The first contentful paint is faster with server-side rendering
Basically, you are shipping a static landing page to the user until the rest of the application is loaded. Even with small differences in the load time and first paint of 100ms, you can notice a real change in the conversions and user interaction for your applications.
Server-side rendering improves the SEO of your application
With server-side rendering, you are rendering all HTML content on the server giving the web crawlers complete HTML pages to work with and index. With a regular SPA, you have the risk that your pages are not indexed or found by web crawlers or are seen as empty pages with no content. Not only will server-side rendering ensure HTML content can be indexed, but it also allows for different Meta and Title tags on your HTML pages further improving the SEO of your application. We will dive a bit deeper into this later in the guide.
Let's start building our Angular Universal App
We start by creating a new Angular application with the Angular CLI.
ng new angular-universal-test-app
With this command, the Angular CLI will scaffold a new default Angular application for you. Open the newly created App in your preferred code editor. The result will look something like this:
We will not be diving into the structure of the Angular application created by the CLI as that falls out of the scope of this guide. Let's move on by starting the application with the following command at the root of your project:
ng serve -o
The command will start the Angular development server and open a browser window on localhost:4200 with your Angular application running. You should see something like this:
Now let's inspect a few things to get a better idea of what is happening and what will be different when we use Angular Universal and server-side rendering. First, we open the DevTools from our browser by pressing F12. Now go to the network tab and make sure 'All' is selected in the filter bar. Refresh the page in the browser and you should see something like this in your DevTools:
When you open the 'response' tab you will see the HTML shipped to the browser, it will look something like this:
Another thing we are missing for SEO purposes is a page title and description. When we search for something the search engine shows results in a format of a title and description. The values for this title and description are found in the meta and title tags inside the head of your HTML page. With a regular SPA application, these tags are injected and evaluated at runtime. Just as with the content a lot of search engine crawlers expect these tags to be there when the HTML page is loaded and do not index values injected at runtime.
When we use server-side rendering these HTML tags will be included in the HTML on the server before it's shipped to the browser. That way web crawlers can index it and show the correct title and descriptions for your indexed search results.
Let's move to the 'Performance insight' tab in the browser DevTools and click on "Measure page load"
Enough talk, let's continue to build!
As with most things Angular the heavy lifting is done for us and we can utilize the Angular CLI to convert our regular Angular application into an Angular Universal servicer-side rendered application. To convert your Angular app into an SSR app run the following command:
ng add @nguniversal/express-engine
Running the above command is basically everything you need to do to convert your Angular application into a server-side rendered application, but some extra information might be useful. The command added and update some files in your Angular application.
First, let's have a look at our angular.json file. The angular.json is one of the files updated by the Angular CLI. It added a couple of build configurations and changed the output path for the build configuration. The newly added configurations are for your server configuration, development SSR server configuration, and the prerender configuration. A bit more information on this later in the guide. For the 'Build' configuration the output path is updated so your build output goes into a folder named browser. The new server build configuration outputs its files to a server folder inside your dist (next to the browser folder). The updated angular.json looks like this:
In your package.json a couple of new scripts and packages are added after running the CLI command. The important part is the newly added scripts. We will dive into more detail on when and how to use these new scripts in a bit.
There are also a couple of files added by the CLI command, one of them is the entry point for our server application main.server.ts. It's similar to the entry point for the client-side rendered Angular app in main.ts, the difference being that we export the AppServerModule instead of the AppModule.
Let's have a look at the newly created AppServerModule. Again it's very similar to the AppModule. In fact, we import the AppModule into the AppServerModule so the AppServerModule has all the same components and modules as the AppModule with one addition the ServerModule from @angular/platform-server. This ServerModule will swap some internal Angular services mainly for rendering purposes but for the rest, everything is the same for your client-side and server-side rendered applications.
There are a couple more small changes made but the last important piece to the puzzle is the server.ts file added by the CLI command. This server.ts file contains the server that will render out HTML pages. It's a simple Express.js server that will render out HTML pages before sending them to the browser. The important part of the server is the ngExpressEngine() implementation. This function is a wrapper implementation around the render module from Angular Universal and it turns client requests into server-side rendered HTML pages.
Now that we have a better understanding of what the CLI command added and updated in our Angular application, let's have a look at how we can serve and build our SSR application and what the difference in the browser is. We can serve our SSR Angular application with a development server or with our own server.ts file. Let's first start it with the development server by running the following command:
npm run dev:ssr
This command will spin up a development server that will server-side render your HTML pages before sending them to the browser. This command will not make use of your own server.ts file and is not meant to use in production. The server will start on localhost:4200 similar to when you serve your regular SPA application and uses a development build of the application (no dist folder is created with a production build of your server and browser files).
When we open the DevTools in the browser and have a look at the 'localhost' request for the index.html we can see something different than before. Before we converted our Angular application into an SSR application we only saw a white screen when we inspected the preview tab, now we can see an actual preview of the page.
Also when we inspect the response tab in the DevTools we can see the HTML now reflects the page we see (and not an empty page like before). When a web crawler of a search engine now hits your page it will receive the full content of the page giving it the opportunity to index it correctly and include it in its search results.
To server-side render other pages, there is nothing extra you have to do. Just add the routes and pages to your application and the server will pick them up, server-side render them, and provide it to the browser. Remember only the initially requested page is server-side rendered. After the application is loaded and the 'landing page' is rendered your regular SPA application takes over again.
You saw the difference in the browser and know how to serve our SSR Angular Universal app in a development environment, let's have a look at how we can use our server.ts and build our SSR application for a production environment.
First, we need to build our application using the following command:
npm run build:ssr
This command will build our client and server application and output it to a dist folder. Inside this dist folder, we find a browser folder containing our client-side code and a server folder containing our Express.js server for rendering our HTML pages.
Now to start our Angular Universal SSR application we run the following command:
npm run serve:ssr
This command will start your Express.js server on port 4000. If you now navigate to http://localhost:4000 in your browser you will see the Angular application again. This time served in production mode with our production server located in our dist folder.
Note that when you deploy your SSR application to Azure or some other hosting service it can not be hosted as a static website, but it needs to be hosted as a server application.
Prerndering static pages
The last command added to our package.json is the prerender command. With this command Angular Universal lets, you compile pages into pre-rendered static HTML pages at build time. Meaning you will already have the rendered HTML pages in your build drop-off in the browser folder of your dist. This is also known as static site generation.
The advantage of prerendering is that the HTML file doesn't have to be rendered on the server making it available to the browser slightly faster and allowing you to host it cheaper and more reliable with a CDN. It also gives you a bit more control over the HTML file since you have it in your build output. All this can contribute to an even better user experience and faster load times. A drawback might be that your build output gets larger and the build process takes a bit longer. You can prerender you complete (static) website and host it as is, or you can mix and match with your Angular universal server that sends the prerendered pages when they are available and otherwise renders the page on the server.
You can prerender pages by running the following command:
ng run <your-app-name>:prerender --routes /blog/1 /blog/2
The --routes parameter in the command can be omitted, in this case, the prerendering will take a lot more time and it might miss pages you wanted to be rendered. When you omit the --routes parameter Angular will guess routes and when it finds an existing one it gets prerendered. You can also provide routes with a file, if you have a lot of them you want to prerender this might be your best option. Just create a routes.txt file in the root of your project containing all your routes and run this command:
ng run <app-name>:prerender --routes-file routes.txt
For more information on prerendering, you can have a look at the Angular docs.
Improving our Angular Universal SSR application
We can still make a few improvements to our Angular Universal application so we do even better on the SEO front and offer a better user experience. Earlier we talked about the title tag and meta tags that need to be included in our HTML pages for SEO purposes. When we search for something in a search engine the results are displayed with a title and description. To tell the search engine what title and description to display we need to add a meta tag for the description and a title tag for the tile. Both HTML tags need to be included in the head tag f the HTML page.
Prevent double requests with Angular TransferState API
When we use Angular Universal and server-side rendering we introduce a new 'problem'. When we render the page on the server the requests needed to build the page are already made on the server, after we send the HTML to the front-end and the regular SPA takes over it will again make the request to fetch the data and repaint it. Often times this results in a flickering effect on the first-page load and this is not something we want or considered a good user experience.
What we need here is a mechanism that caches the requests made on the server and transfers this state to the regular SPA application when Angular Universal swaps them. This way no double requests are made. Lucky for us Angular provides us with a tool to do just that and it's called TransferSate.
The way it works is; you register a unique string value under which you save the cached values. This value is called the masterStateKey and each object you fetch should have its own masterStateKey under which it's saved. Next, you can use TranserState to get, set, or remove objects a bit like with localStorage.
Below is an example of how you can use TransferState. After you implement this the flickering will be gone and requests will not be made by the SPA on page load but only on the server side.
Angular Universal is a good way to improve your initial page load and overall SEO performance. Implementing server-side rendering is handled with a single command and maybe some improvements for an even better experience. If you are building a blog, webshop, or any other website where the page load times, the initial browser paints, and SEO is important adding Angular Universal and server-side rendering most likely is the way to go for you.