Pre-rendering in Angular 9

Shashikant Devani

Jun 12, 2020 | 5 min read

What is Pre-rendering in angular?

Pre-rendering creates a static page when we build an angular application. As a result, the website will load faster and will be SEO friendly without the need for server-side rendering. The pre-rendering is helpful if we want to serve static pages in the application.

Why should we pre-render Angular applications?

Most of the obstacles that one faces with an Angular application are fixed by following the optimization techniques. Although there are a still a few problems which optimizations won’t fix:

  1. SEO (search engine optimization): SPAs (single-page applications) are harder to index by search engines because the content isn’t available on load time. Therefore, the application is likely to fail on several SEO requirements.
  2. Slow initial page load: Since the application still needs to be bootstrapped after the page is loaded, there is an initial waiting time until the user can use the application. This results in a bad user experience.

Now, we have two way to solve this problem.

  1. Server Side Rendering (SSR): SSR executes the Angular application on the server. The server will serve the compiled content in a way that search engine crawlers can read it. SSR is the best of both worlds. The server will render the application, but when the JavaScript bundle is loaded, it will turn into a SPA. The result is an application with rich UI/UX that loads quickly at the same time!
  2. Pre-Rendering: Pre-rendering would run generated static HTML files when we create build,  and that is insanely fast. And when the JavaScript bundles are loaded, the browser would take over. What we get after this is Ultra fast loading time + No compromise in the rich SPA experience.

Both the technique solves the issue we have above, but if you see the rendering time that we have observed, it will make sense to choose the later.

Effective load time for Pre-Rendering and SSR

How to implement pre-render Angular applications?

First, you need to add angular universal to your project using this command from server-side rendering guide

Must use Node 12 or above Version

ng add @nguniversal/express-engine --clientProject project-example

The command will generate all the scripts to run server-side rendering in our application. You need i itt to generate static pages.

The next step is to transfer the code fragments responsible for creating the express server from server.ts to a new express-app.ts file. Below is the code that you should paste into the new file.

// express-app.ts


import 'zone.js/dist/zone-node';

import * as express from 'express';
import { join } from 'path';

// Express server
export const app = express();

const DIST_FOLDER = join(process.cwd(), 'dist/browser');

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap} = require('./dist/server/main');

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));

app.set('view engine', 'html');
app.set('views', DIST_FOLDER);

// Example Express Rest API endpoints
// app.get('/api/**', (req, res) => { });
// Serve static files from /browser
app.get('*.*', express.static(DIST_FOLDER, {
  maxAge: '1y'
}));

// All regular routes use the Universal engine
app.get('*', (req, res) => {
  res.render('index', { req });
});

Once you copy the code remove the express server code from the server.ts and instead import it from the express-app.ts leaving only the code listening to the port.

// server.ts


import { app } from './express-app';

const PORT = process.env.PORT || 4000;

// Start up the Node server
app.listen(PORT, async () => {
  console.log(`Node Express server listening on http://localhost:${PORT}`);
});

Further, now add the main code that performs pre-render of our sub-pages. (you also need to install an additional library to create sub-folders using the command   npm install mkdirp @types/mkdirp)

// Prerender.ts

import * as request from 'request-promise';
import * as mkdirp from 'mkdirp';
import { promisify } from 'util';
import { writeFileSync } from 'fs';
import { Express } from 'express';

import { app } from './express-app';

export const ROUTES = [
  '/',
  '/auth',
  '/privacy-policy'
];

const mkdirpAsync = promisify(mkdirp);

function prerender(expressApp: Express, routes: string[]) {
  const PORT = process.env.PRERENDER_PORT || 4000;
  // Start up the Node server
  const server = expressApp.listen(PORT, async () => {
    try {
      for (const route of routes) {
        const result = await request.get(`http://localhost:${PORT}${route}`);
        await mkdirpAsync(`dist/browser${route}`);
        writeFileSync(`dist/browser${route}/index.html`, result);
      }
      console.log('Prerender complete.');
      server.close();
    } catch (error) {
      server.close(() => process.exit(1));
    }
  });
}

prerender(app, ROUTES);
// Work around Solution for https://github.com/angular/angular-cli/issues/7200 (This is just for reference)

const path = require('path');
const webpack = require('webpack');

module.exports = {
  mode: 'none',
  entry: {
    // This is our Express server for Dynamic universal
    server: './server.ts',
    prerender: './prerender.ts' // <--------------- HERE!!!
  },
  externals: {
    './dist/server/main': 'require("./server/main")'
  },
  target: 'node',
  resolve: { extensions: ['.ts', '.js'] },
  optimization: {
    minimize: false
  },
  output: {
    // Puts the output at the root of the dist folder
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  },
  module: {
    noParse: /polyfills-.*\.js/,
    rules: [
      { test: /\.ts$/, loader: 'ts-loader' },
      {
        // Mark files inside `@angular/core` as using SystemJS style dynamic imports.
        // Removing this will cause deprecation warnings to appear.
        test: /(\\|\/)@angular(\\|\/)core(\\|\/).+\.js$/,
        parser: { system: true }
      }
    ]
  },
  plugins: [
    new webpack.ContextReplacementPlugin(
      // fixes WARNING Critical dependency: the request of a dependency is an expression
      /(.+)?angular(\\|\/)core(.+)?/,
      path.join(__dirname, 'src'), // location of your src
      {} // a map of your routes
    ),
    new webpack.ContextReplacementPlugin(
      // fixes WARNING Critical dependency: the request of a dependency is an expression
      /(.+)?express(\\|\/)(.+)?/,
      path.join(__dirname, 'src'),
      {}
    )
  ]
};

You will need to add a new script to package.json for ease of use.


"scripts": {
  ...
  "prerender": "node dist/prerender",
  ...
}

And that’s all, now you just need to build an application in SSR mode and run the command "prerender".

npm run build:ssr && npm run prerender

In the dist/browser directory, you will find sub-folders with index.html files containing SEO-friendly generated HTML content of the application.

If you are using @angular/service-worker then you will need to reconfigure right after the prerender because the checksum value in the index.html file has changed after the html modification. Use the command below to do so.

ngsw-config dist/browser ngsw-config.json

Conclusion

Pre-render allows us to generate html files for every route so that we can serve content faster.

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.


Third Rock Techkno is a leading IT services company. We are a top-ranked web, voice and mobile app development company with over 10 years of experience. Client success forms the core of our value system.

We have expertise in the latest technologies including angular, react native, iOs, Android and more. Third Rock Techkno has developed smart, scalable and innovative solutions for clients across a host of industries.

Our team of dedicated developers combine their knowledge and skills to develop and deliver web and mobile apps that boost business and increase output for our clients.