angular/aio/content/guide/universal.md
Ward Bell 44c7469495 docs: Migrate Universal guide and code to Standalone (#51564)
Heavily reworked with advice from Alan Agius.

FWIW
* `ng test` fails because there are no unit tests
* `yarn e2e` builds but fails - `Unable to start a WebDriver session.`

**Added `readme.md` to sample code**

Makes it easier for the casual downloader to try the example.

**Added `universal-ngmodule`**

The existing ngModule-based doc has a lot of guidance for developers who are still building with NgModule. Given that we are preserving the existing NgModule guidance (with modifications), it seems prudent to preserve the NgModule form of the Universal guidance. So I copied the existing guide and code to `universal-ngmodule` and added it to the left-side navigation under the "NgModules" sub-tree.

Warning: it retains all of the faults of the original guide ... faults that were addressed in the current revision.

PR Close #51564
2023-09-12 12:20:10 -07:00

24 KiB

Server-side rendering (SSR) with Angular Universal

This guide describes Angular Universal, a technology that allows Angular to render applications on the server.

By default, Angular renders applications only in a browser. Angular Universal allows Angular to render an application on the server, generating static HTML content, which represents an application state. Once the HTML content is rendered in a browser, Angular bootstraps an application and reuses the information available in the server-generated HTML.

With server-side rendering an application generally renders in a browser faster, giving users a chance to view the application UI before it becomes fully interactive. See the "Why use Server-Side Rendering?" section below for additional information.

Also for a more detailed look at different techniques and concepts surrounding SSR, check out this article.

You can enable server-side rendering in your Angular application using the @nguniversal/express-engine package as described below.

Angular Universal requires an active LTS or maintenance LTS version of Node.js. For information see the version compatibility guide to learn about the currently supported versions.

Universal tutorial

The Tour of Heroes tutorial is the foundation 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.

Download the finished sample code, which runs in a Node.js® Express server.

This is the guide for Angular Universal with Standalone Applications. If your application is built with NgModules, see Angular Universal applications with NgModules.

Step 1. Enable Server-Side Rendering

Run the following Angular CLI command to add SSR support into your application:

ng add @nguniversal/express-engine

The command updates the application code to enable SSR and adds extra files to the project structure (files that are marked with the * symbol).

src
index.html                             // <-- app web page
main.ts                                  // <-- bootstrapper for client app
main.server.ts                       // <-- * bootstrapper for server app
style.css                                // <-- styles for the app
app/  …                                   // <-- application code
app.config.ts                   // <-- client-side application configuration
app.config.server.ts       // <-- * server-side application configuration
app.routes.ts                   // <-- client-side application routes
server.ts                                // <-- * express web server
tsconfig.json                        // <-- TypeScript base configuration
tsconfig.app.json                // <-- TypeScript browser application configuration
tsconfig.server.json            // <-- TypeScript server application configuration
tsconfig.spec.json              // <-- TypeScript tests configuration

This CLI command succeeds only if your standalone application is bootstrapped in the recommended way with app.config.ts and app.routes.ts files.

If your Angular application doesn't follow this practice, you can quickly and easily refactor to that practice, as explained below, before you run the command.

Step 2. Enable Client Hydration

Hydration is the process that restores the server side rendered application on the client. This includes things like reusing the server rendered DOM structures, persisting the application state, transferring application data that was retrieved already by the server, and other processes. Learn more about hydration in this guide.

The hydration feature is available for developer preview. It's ready for you to try, but it might change before it is stable.

You can enable hydration by updating the app.config.ts file.

Import the provideClientHydration function from @angular/platform-browser and add the function call to the providers section as shown below.

Step 3. Start the server

To start rendering your application with Universal on your local system, use the following command.

npm run dev:ssr

Step 4. Run your application in a browser

Once the web server starts, 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 the built-in anchor `<a>` elements. You can go from the Dashboard to the Heroes page and back. Click a hero on the Dashboard page to display its Details page.

If you throttle your network speed so that the client-side scripts take longer to download instructions following, you'll notice:

  • You can't add or delete a hero
  • The search box on the Dashboard page is ignored
  • The Back and Save buttons on the Details page don't work

The transition from the server-rendered application to the client application happens quickly on a development machine, but you should always test your applications in real-world scenarios.

You can simulate a slower network to see the transition more clearly as follows:

  1. Open the Chrome Dev Tools and go to the Network tab.
  2. Find the Network Throttling dropdown on the far right of the menu bar.
  3. Try one of the "3G" speeds.

The server-rendered application still launches quickly but the full client application might take 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 might be unable to navigate and index your highly interactive Angular application as a human user could do.

Angular Universal can generate a static version of your application that is easily searchable, linkable, and navigable without JavaScript. Universal also makes a site preview available because each URL returns a fully rendered page.

Improve performance on mobile and low-powered devices

Some devices don't support JavaScript or execute JavaScript so poorly that the user experience is unacceptable. For these cases, you might require a server-rendered, no-JavaScript version of the application. This version, however limited, might be the only practical alternative for people who otherwise couldn't use the application at all.

Show the first page quickly

Displaying the first page quickly can be critical for user engagement. Pages that load faster perform better, even with changes as small as 100ms. Your application might have to launch faster to engage these users before they decide to do something else.

With Angular Universal, you can generate landing pages for the application that look like the complete application. The pages are pure HTML, and can display even if JavaScript is disabled. The pages don't handle browser events, but they do support navigation through the site using routerLink.

In practice, you'll serve a static version of the landing page to hold the user's attention. At the same time, you'll load the full Angular application behind it. The user perceives near-instant performance from the landing page and gets the full interactive experience after the full application loads.

Universal web servers

A Universal web server responds to application page requests with static HTML rendered by the Universal template engine. The server receives and responds to requests from clients usually browsers, and serves static assets such as scripts, CSS, and images. It might respond to data requests, either directly or as a proxy to a separate data server.

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 don't rely on a browser.

The web server for this guide is Node.js Express, configured to pass client requests for application pages to Angular's ngExpressEngine. This engine renders the app while also providing caching and other helpful utilities.

The engine's render function takes as inputs a template page usually `index.html`, a function returning a Promise that resolves to an ApplicationRef, and a route that determines which components to display. The route comes from the client's request to the server.

Each request results in the appropriate view for the requested route. The render function renders the view within the <app> tag of the template, creating a finished HTML page for the client.

Finally, the server returns the rendered page to the client.

Caching data when using HttpClient

By default, the provideClientHydration() function enables the recommended set of features for the optimal performance for most of the applications. It includes the following features:

  • Reconciling DOM hydration (learn more about it here).
  • HttpClient response caching while running on the server and transferring this cache to the client to avoid extra HTTP requests.

While running on the server, data caching is performed for every HEAD and GET requests done by the HttpClient. After that this information is serialized and transferred to a browser as a part of the initial HTML sent from the server after server-side rendering. In a browser, HttpClient checks whether it has data in the cache and if so, reuses it instead of making a new HTTP request during initial application rendering. HttpClient stops using the cache once an application becomes stable while running in a browser.

Working around the browser APIs

Because a Universal application doesn't execute in the browser, some of the browser APIs and capabilities might be missing on the server.

For example, server-side applications can't reference browser-only global objects such as window, document, navigator, or location.

Angular provides some injectable abstractions over these objects, such as Location or DOCUMENT; it might substitute adequately for these APIs. If Angular doesn't provide it, it's possible to write new abstractions that delegate to the browser APIs while in the browser and to an alternative implementation while on the server also known as shimming.

Similarly, without mouse or keyboard events, a server-side application can't rely on a user clicking a button to show a component. The application must determine what to render based solely on the incoming client request. This is a good argument for making the application routable.

Universal and the Angular Service Worker

If you are using Universal in conjunction with the Angular service worker, the behavior is different than the normal server side rendering behavior. The initial server request will be rendered on the server as expected. However, after that initial request, subsequent requests are handled by the service worker. For subsequent requests, the index.html file is served statically and bypasses server side rendering.

Universal template engine

The server.ts file configures the Universal template engine. The core of a typical implementation looks like this:

Focus on the ngExpressEngine() function which turns a client's requests for Angular pages into server-rendered HTML pages.

The function accepts an object with the following properties:

Properties Details
bootstrap The bootstrapping function that returns a Promise resolving to an ApplicationRef for the application to render on the server. It's the bridge between the Universal server-side renderer and the Angular application.
extraProviders This optional property lets you specify dependency providers that apply only when rendering the application on the server. Do this when your application needs information that can only be determined by the currently running server instance.

The bootstrap function comes from main.server.ts, generated by the CLI command that created your universal app.

The ngExpressEngine() function returns a Promise callback that resolves to the rendered page. The web server resolves the promise and forwards the page to the client.

Filtering request URLs

By default, if the application were only rendered by the server, every application link clicked would arrive at the server as a navigation URL intended for the router.

However, most server implementations have to handle requests for at least three very different kinds of resources: data, application pages, and static files. Fortunately, the URLs for these different requests are easily recognized.

Routing request types Details
Data request Request URL that begins /api
App navigation Request URL with no file extension
Static asset Request URL with file extension

The server.ts generated by the CLI already makes these basic distinctions. You may have to modify it to satisfy your specific application needs.

Serving Data

A Node.js Express server is a pipeline of middleware that filters and processes requests one after the other.

For data requests, you could configure the Node.js Express server pipeline with calls to server.get() as follows:

This guide's server.ts doesn't handle data requests. It returns a 404 - Not Found for all data API requests.

For demonstration purposes, this tutorial intercepts all HTTP data calls from the client before they go to the server and simulates the behavior of a remote data server, using Angular's "in-memory web API" demo package.

In practice, you would remove the following "in-memory web API" code from app.config.ts.

Then register your data API middleware in server.ts.

App Navigation

Application routes look like this: /dashboard, /heroes, /detail:12. While they have a lot of variety, what they all have in common is no file extension.

The following code filters for request URLs with no extensions and treats them as navigation requests:

Serving Static Files Safely

All static asset requests such as for JavaScript, image, and style files have a file extension (examples: main.js, assets/favicon.ico, src/app/styles.css). They won't be confused with navigation or data requests if you filter for files with an extension.

To ensure that clients can only download the files that they are permitted to see, put 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 requests for files with an extension (*.*) to /dist, and returns a 404 - NOT FOUND error if the file isn't found.

Useful scripts

Scripts Details
npm run dev:ssr Similar to ng serve, which offers live reload during development, but uses server-side rendering. The application runs in watch mode and refreshes the browser after every change. This command is slower than the actual ng serve command.
ng build && ng run app-name:server Builds both the server script and the application in production mode. Use this command when you want to build the project for deployment.
npm run serve:ssr Starts the server script for serving the application locally with server-side rendering. It uses the build artifacts created by npm run build:ssr, so make sure you have run that command as well.
serve:ssr is not intended to be used to serve your application in production, but only for testing the server-side rendered application locally.
npm run prerender Used to prerender an application's pages. Read more about prerendering here.

Bootstrapping the client with app.config.ts

The CLI command to generate a Universal Application assumes that your standalone application'smain.ts bootstraps with the appConfig object exported from your app.config.ts like this.

If your existing app doesn't follow this practice, you can quickly refactor before running the CLI command.

Refactoring Example

Suppose your standalone application has a do-everythingmain.ts something like this:

It defines all of the application routes and bootstraps with an inline ApplicationConfig object that lists providers. You'll need to extract these configuration details into their own files.

  1. Extract the routes into a separate file called app.routes.ts:

  1. Extract the configuration object into a separate file called app.config.ts:

  1. Reduce main.ts to a streamlined form that references the appConfig object exported by app.config.ts:

Why this is a good idea

The Universal server needs the same configuration as your client. You'd prefer to maintain that configuration in one place so that configuration stays in sync on both the client and the universal server.

This simplicity also enables the CLI command to generate a main.server.ts that looks so much like main.ts:

The exported bootstrap function is exactly what the ngExpressEngine() function requires in server.ts.

@reviewed 2023-08-29