Angular server side rendering

"For every 100 ms of improvement, they grew incremental revenue by up to 1%"
Because of this aspect, it is extremely important to implement very fast page load speeds. There are a few techniques that all have different benefits. For example, when websites are created as progressive web applications, they introduce caches that reuse data and patch incoming data in the background. This results in a large increase in load times except for the first page load. Other approaches, e.g. prevent the loading of images outside the screen. The use of all techniques in combination gives a good result. Therefore, we should not miss the server-side rendering.

Requirements

To implement server-side rendering, you must have access to a Nodejs-enabled Web server. For example, I like to use Heroku because of its simplicity. In addition, you must have an existing Angular application for it to be rendered server-side. This article recommends Angular 6 projects and I do not think this works the same way with earlier versions.
Assuming you have got an existing Angular project with a similar folder structure as above, this article describes how to make it server-side rendering capable.

Dependencies

At first install required dependencies @nguniversal/express-engine and @nguniversal/module-map-ngfactory-loader using the following command.
npm i @nguniversal/express-engine @nguniversal/module-map-ngfactory-loader
In addition I will use express as NodeJS webserver. So also do npm i express.

Create the server

Let us first create the api function in the api.ts file, which requires the distPath, the Angular build path, and the Angular Universial options. This file contains the very basic and minimal function and does for example not include SSL enforcement and compression.
import { ngExpressEngine, NgSetupOptions } from '@nguniversal/express-engine'; import { join } from 'path'; import * as express from 'express'; export function createApi(distPath: string, ngSetupOptions: NgSetupOptions) { const app = express(); app.set('view engine', 'html'); app.set('views', distPath); // Angular Express Engine. app.engine('html', ngExpressEngine(ngSetupOptions)); app.get('*.*', express.static(distPath)); // Redirect all reqular routes to the index file. app.get('*', (req, res) => { res.render(join('..', 'browser', 'index'), { req }); }); return app; }
Listing: api.ts
The next thing to do is the server implementation in the main.server.ts file.
import 'zone.js/dist/zone-node'; import 'reflect-metadata'; import { enableProdMode } from '@angular/core'; import { MODULE_MAP } from '@nguniversal/module-map-ngfactory-loader'; import { join } from 'path'; import { createServer } from 'http'; import { createApi } from './api'; export const PORT = process.env.PORT || 61624; export const BROWSER_DIST_PATH = join(__dirname, '..', 'browser'); enableProdMode(); export const getNgRenderMiddlewareOptions = () => ({ bootstrap: exports.AppServerModuleNgFactory, providers: [{ provide: MODULE_MAP, useFactory: () => exports.LAZY_MODULE_MAP, deps: [] }] }); export { AppServerModule } from './app/app/app.server.module'; const requestListener = createApi(BROWSER_DIST_PATH, getNgRenderMiddlewareOptions()); const server = createServer((req, res) => { requestListener(req, res); }); server.listen(PORT, () => { console.log(`Server listening -- http://localhost:${PORT}`); }); export default server;
Listing: main.server.ts
Ouch. Lets destruct it step by step. At first we are importing all required plugins. Afterwards we define the port and the distribution path of the Angular project mentioned earlier in the api creating function followed by enabling production mode, which disables unidirectional dataflow in the Angular application, read more. In the next step we are implementing a function which returns an data object looking similar to the NgModule decorator input. There is the bootstrap and the providers attribute. So we are bootstrapping an ApplicationModuleNgFactory. Digging deeper there is the compiled main.js output with
var app_server_module_ngfactory_1 = __webpack_require__(/*! ./app/app.server.module.ngfactory */ "./src/app/app/app.server.module.ngfactory.js"); exports.AppServerModuleNgFactory = app_server_module_ngfactory_1.AppServerModuleNgFactory;
Listing: Extract from main.js
We're telling the server to use Angular-Universal generated module maps instead of lazy-loading routes. The next steps are pretty simple. Exporting the AppServerModule, create a requestListener with the distribution path and the rendering options as parameters, set up an http server with the requestListener listening for incoming requests and make the server listen to request on PORT.

Set up Angular CLI

Make sure to add the Universal dev kit to the developer dependencies
npm i --save-dev udk
Afterwards go to your angular.json file and add the following parts to the architects section (the section where the build and serve sections are located).
"server": { "builder": "@angular-devkit/build-angular:server", "options": { "outputPath": "dist/ssr", "main": "src/main.server.ts", "tsConfig": "src/tsconfig.server.json" }, "configurations": { "production": { "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ] } }
Listing: angular.json
and
"udk": { "builder": "udk:udk-builder", "options": { "browserTarget": "blog:build", "serverTarget": "blog:server" }, "configurations": { "production": { "browserTarget": "blog:build:production", "serverTarget": "blog:server:production" } } }
Listing: angular.json
"udk-server": { "builder": "udk:udk-builder", "options": { "serverTarget": "blog:server" }, "configurations": { "production": { "serverTarget": "blog:server:production" } } }
Listing: angular.json
Don't forget to change blog to your applications ID (the direct child of projects section).

Add a build script

Add the following script to your package.json file and don't forget to replace blog with your application ID again.
"scripts": { "start": "node ./dist/ssr/main.js", "build": "ng run blog:udk-server:production" }

Build and run

Finally simply run and watch the compilation (hopefully) succeeding.
npm run build
Your output should be located under dist/browser for your regular Angular application and the server files should be placed in dist/ssr. Test server side rendering by running
npm run start
and visit the http://localhost:PORT with the browser. For checking the server side rendering disable JavaScript in your browser since Angular requires JavaScript to boot up properly. Server-side rendering directly renders the html and sends it to the client so that no JavaScript is needed.

Comments

Any questions? Want to join the discussion?

Sign in or sign up