Single-page Applications (SPAs) consist of a single HTML document that is served initially to a client. Any new views that are required in the app are generated solely on the client via JavaScript. The request-response cycle still happens, but this is usually only to RESTful APIs for data; or to get static resources, like images.
There are a lot of benefits to writing applications this way. However, there are some things that you lose, such as the ability for web crawlers to traverse your app and slower performance while the app is loading, which can take a significant amount of time. In comes Server-Side Rendering (SSR) to bridge the gap.
In this article, you will learn how to use Angular Universal for Server-Side Rendering.
To follow along with this tutorial, you will need:
This tutorial was verified with Node v16.4.2, npm
v7.19.1, @angular/core
v12.1.1, and @nguniversal/express-engine
v12.1.0.
An Angular application is a Single-page App - it runs in a client’s browser. Angular Universal, however, lets you also run your Angular app on the server. This enables you to serve static HTML to the client. With Angular Universal, the server will pre-render pages and show your users something, while the client-side app loads in the background. Then, once everything is ready client-side, it will seamlessly switch from showing the server-rendered pages to the client-side app. Your users shouldn’t notice a difference, beyond the fact that instead of waiting for your “Loading” spinner to finish, they at least can have some content to keep them engaged until they can start using the fully-featured client-side application.
SSR with Angular Universal requires changes in both the client application and the server stack to work. For this article, we’ll assume this is a brand new Angular application, barely created with the Angular CLI, at version >= 7. Just about any server technology can run a Universal app, but it has to be able to call a special function, renderModuleFactory()
, provided by Angular Universal, which is itself a Node package; so serving this Angular application from a Node/Express server makes sense for this example.
We’ll be using a schematic to get us up and going quickly.
From your app directory open a terminal and run the following command:
- ng add @nguniversal/express-engine
You’ll notice this schematic made several changes to your app, modifying some files and adding some files:
angular.json
projects.{{project-name}}.architect.build.options.outputPath
changes to "dist/browser"
projects.{{project-name}}.architect
is added, called "server"
This lets the Angular CLI know about our server/Universal version of the Angular application.
package.json
Besides a few new dependencies (to be expected), we also get a few new scripts:
"dev:ssr"
"build:ssr"
"serve:ssr"
"prerender"
main.ts
This has been modified so that the browser version of the app won’t start bootstrapping until the Universal-rendered pages have been fully loaded.
app.module.ts
Modified to execute the static method .withServerTransition
on the imported BrowserModule
. This tells the browser version of the application that the client will be transitioning in from the server version at some point.
server.ts
This is the NodeJS Express server. Obviously, you don’t have to use this exact server setup as generated, although make note of the line:
server.engine('html', ngExpressEngine({ ... }))
ngExpressEngine
is a wrapper around renderModuleFactory
which makes Universal work. If you don’t use this exact server.ts
file for your set-up, at least copy this part out and integrate it into yours.
tsconfig.server.json
This tells the Angular compiler where to find the entry module for the Universal application.
app.server.module.ts
This is the root module for the server version only. You can see it imports our AppModule
, as well as the ServerModule
from @angular/platform-server
, and bootstraps the same AppComponent
as AppModule
. AppServerModule
is the entry point of the Universal application.
main.server.ts
This new file basically only exports the AppServerModule
, which is the entry point of the Universal version of the application. We’ll revisit this soon.
From a command line, run the following command:
- npm run build:ssr
And then run:
- npm run serve:ssr
Assuming you didn’t hit any snags during the build process, open your browser to http://localhost:4000
(or whatever port is configured for you), and you should see your Universal app in action! It won’t look any different, but the first page should load much quicker than your regular Angular application. If your app is small and simple, this might be hard to notice.
You can try throttling the network speed by opening Chrome Dev Tools, and under the Network tab, finding the dropdown that says Online. Select Slow 3G to mimic a device on a slow network - you should still see good performance for your landing page, and any routed pages you go to.
Also, try viewing the page source (right-click on the page and select View Page Source). You’ll see all normal HTML in the <body>
tag that matches what is displayed on your page - meaning, your application can be meaningfully scraped by a web crawler. Compare this with the page source of a non-Universal application, and all you’ll see in the <body>
tag is <app-root>
(or whatever you’ve called the selector for your bootstrapped AppComponent
).
In this article, you learned how to use Angular Universal for Server-Side Rendering.
Since a Universal application runs on the server and not in a browser, there are a few things you need to watch out for in your application code:
window
, document
, or location
. These don’t exist on the server. You shouldn’t be using these anyway; try using an injectable Angular abstraction, such as Document
or Location
. As a last resort, if you do truly need them, wrap their usage in a conditional statement, so that they’ll only be used by Angular on the browser. You can do this by importing the functions isPlatformBrowser
and isPlatformServer
from @angular/common
, injecting the PLATFORM_ID
token into your component, and running the imported functions to see whether you’re on the server or the browser.ElementRef
to get a handle on an HTML element, don’t use the nativeElement
to manipulate attributes on the element. Instead, inject Renderer2
and use one of the methods there.routerLink
will work for navigation.setTimeout
, where possible.Continue learning about what Angular Universal provides in the official documentation.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Is it possible to do this with java Spring Boot backend?
How do i deploy this SSR application to Digital Ocean server with ubuntu/nginx. The tutorial works well on local machine. What about production
Such a Great Article !!!