Angular Server-side rendering (SSR) with Angular Universal
A typical Angular application executes in the browser, rendering pages in the DOM in response to user actions. Angular Universal executes on the server, generating the static application page which is then bootstrapped on the client. This means that the application typically renders more quickly, giving users a chance to see the application layout before it is fully interactive.
You can easily prepare an application for server-side rendering using the Angular CLI. The CLI schematic @nguniversal/express-engine follows the necessary steps, as described below.
Angular Universal requires an active LTS or maintenance LTS version of Node.js. Check the engine property in the package.json file to find out about the currently supported versions.
Note: Download the ready sample code, which runs in a Node.js® Express Server.
The Tour of Heroes tutorial is the basis for this walkthrough.
In this example, the Angular CLI compiles and bundles the universal version of the application with the Ahead-of-Time (AOT) compiler. A Node.js Express web server compiles HTML pages with Universal based on client requests.
To create a server-side application module, app.server.module.ts, run the following CLI command.
The command creates the following folder structure.
The files marked with * are new and not in the original tutorial sample.
Universal in action
To start rendering your application with Universal on your local system, use the following command.
content_copynpm run dev:ssr
Open a browser and navigate to http://localhost:4200/ . You should see the familiar Tour of Heroes dashboard page.
Navigation using RouterLinks works correctly because they use native anchor (<a>) tags. You can go to the Heroes page and back from the dashboard. You can click on it to display the details page of the hero on the dashboard page.
If you reduce your network speed so that the client-side script takes longer to download (instructions are given below), you will see:
User events other than RouterLinkClick are not supported. You should wait for the events to run or buffer by bootstrapping the full client application and using libraries such as preboot, which allow you to replay these events after the client-side script is loaded.
The transition from a server-rendered application to a client application happens quickly on a development machine, but you should always test your applications in real-world scenarios.
To see the transition more clearly you can simulate a slow network as follows:
Server-provided applications still launch quickly, but full client applications may take a few seconds to load.
Why use server-side rendering?
There are three main reasons to create a Universal version of your application.
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 may be unable to navigate and index your highly interactive Angular application as a human user might.
Improve performance on mobile and low-powered devices
Show the first page quickly.
Displaying the first page quickly can be important for user engagement. Pages that load faster perform better, even with changes as small as 100ms. Your application may have to launch quickly to engage these users before they can decide to do something else.
In practice, you would present a static version of the landing page to capture the user's attention. At the same time, you will load the full Angular application behind it. The user sees performance almost immediately from the landing page and gets a full interactive experience once the full application is loaded.
Universal web servers
Universal Web Server responds to application page requests with static HTML rendered by the Universal Template Engine. The server receives and responds to HTTP requests from the client (usually the browser) and serves static assets such as scripts, CSS, and images. It can respond to data requests, either directly or as a proxy to a separate data server.
The sample web server for this guide is based on the popular Express framework.
Note: Any web server technology can serve a Universal application as long as it can call Universal's readerModule() function. The principles and decision points discussed here apply to any web server technology.
Universal applications use the Angular platform-server package (as opposed to platform-browser), which provides server implementations of the DOM, XMLHttpRequest, and other low-level features that do not depend on the browser.
In this guide's example, the server (Node.js Express in this guide's example) sends client requests for application pages to NgUniversal ngExpressEngine. Under the hood, it calls Universal's ReaderModule() function, providing caching and other useful utilities.
The renderModule() function inputs a template HTML page (usually index.html), an Angular module that contains the components, and how to determine which components to display. The route comes from the request of the client to the server.
Each request results in the appropriate view for the requested route. The renderModule() function renders the view within the template's <app> tag, creating a finished HTML page for the client.
Working around the browser APIs
Because a Universal Application does not execute in the browser, some browser APIs and capabilities may be missing on the server.
For example, server-side applications cannot reference a browser-only global object such as a window, document, navigator, or location.
Angular provides some injectable abstractions on these objects, such as places or documents; It may substitute adequately for these APIs. If Angular doesn't provide this, it is possible to write new abstractions that delegate to the browser API and alternate implementations on the server (aka shimming) while in the browser.
Similarly, a server-side application cannot rely on a user clicking a button to show a component without mouse or keyboard events. The application must determine what to render based on an incoming client request. This is a good argument for making the application runnable.
Universal template engine
The important bit in the server.ts file is the ngExpressEngine() function.
Initial: A Universal Application The ngExpressEngine() function is a wrapper around Universal's ReaderModule() function that turns client requests into server-rendered HTML pages. It accepts an object with the following properties:
Bootstrap: The root NgModule or NgModule factory to bootstrap the application while rendering on the server. For example app, it is AppServerModule. It is the bridge between the universal server-side renderer and the Angular application.
Additional Providers: This is optional and lets you specify dependency providers that are applied when the application is rendered on the server. You can do this when your application requires information that only the currently running server instance can determine.
The ngExpressEngine() function returns a promise callback that gets resolved on the rendered page. It is up to the engine to decide what to do with that page. This engine's promise callback returns the rendered page to the web server, forwarding it to the client in an HTTP response.
Note: These wrappers help to hide the complexity of the renderModule() function. Universal repository has more wrappers for different backend technologies.
For example, --application can give application-configuration such as, for example, document, description, or location.
Filtering request URLs
The basic behavior described below is handled automatically when using the Universal Express schematic. This is helpful when trying to understand the underlying behavior or replicate it without using a schematic.
The web server should separate app page requests from other types of requests.
It's not as simple as intercepting a request for the route address / . The browser may ask for application routes such as /dashboard, /hero, or /details:12. If the server just presented the application, each link clicked would reach the server as a navigation URL for the router.
Fortunately, application routes have something in common: their URLs lack file extensions. All static asset requests have a file extension (such as main.js or /node_modules/zone.js/bundles/zone.umd.js). (Data requests also lack extensions, but they are easy to identify because they always start with /api .)
Because we use routing, we can easily identify the three types of requests and handle them differently.
A Node.js Express server is a pipeline of middleware that filters and processes requests one after the other. You configure the Node.js Express server pipeline with calls to server.get() like this one for data requests.
Note: This sample server doesn't handle data requests.
The tutorial's "In-Memory Web API" module, a demo and development tool, intercepts all HTTP calls and simulates the behavior of a remote data server. In practice, you would remove that module and register your Web API middleware on the server here.
The following code filters for request URLs without extension and treats them as navigation requests.
Serving static files safely.
To ensure that clients can download only the files they are allowed to view, place all client-facing asset files in the /dist folder and only honor requests for files from the /dist folder.
The following Node.js Express code routes all remaining requests to /dist , and returns a 404 - NOT FOUND error if the file is not found.
Using absolute URLs for HTTP (data) requests on the server.
In a server-side rendered app, HTTP URLs must be absolute (for example, https://my-server.com/api/heroes). This means that URLs must be converted to absolute in some way when running on the server and left relative when running in the browser.
If you're using one of the @nguniversal/*-engine packages (like @nguniversal/express-engine), you can take care of that automatically. You don't need to do anything to make relative URLs work on the server.
If you're not using the @nguniversal/*-engine package for some reason, you may have to handle it yourself.
The recommended solution is to pass the full request URL to the options argument of readerModule() or renderModuleFactory() (depending on what you use to render the AppServerModule on the server). This option is the least intrusive as it does not require any changes to the application. Here, "request URL" refers to the URL of the request as a response for which the application is being served on the server. For example, there are options if the client has requested https://my-server.com/dashboard and you are providing an application on the server to respond to that request. url must be set to https://my-server.com/dashboard.
Now, on every HTTP request made to render the application to the server, Angular can correctly resolve the request URL into an absolute URL using the given options. url.
This command is similar to ng serve, which provides live-reload during development, but uses server-side rendering. The application will run in watch mode and refresh the browser after every change. This command is slower than the actual ng serve command.
This command makes both the server script and the application in production mode. Use this command when you want to build project for deployment.
This command starts a server script to serve the application locally with server-side rendering. It uses build artifacts created by ng run build:ssr, so make sure you run that command.
Note that serve:ssr is not intended to serve your application in production, but only to test server-side rendered applications locally.
This script can be used to prerender pages of an application.