Prerender Angular Application using Angular Universal Prerenderer

This article is a second part of the Angular Universal Series.

This series is focusing on improving the initial loading time of angular application as well as adding support for the web crawlers. This helps us to improve SEO and performance on low powered devices.

Angular SEO Series Articles

Note: For this series of articles, We are using Angular 9.

In the previous article, we have seen 

  • Why initial loading time is important?
  • What is the purpose of server-side rendering? 
  • How to implement server-side rendering in the angular application using Angular Universal schematics.

In this article, we will go further with the prerendering angular application with an angular universal prerenderer. This will make your angular application ultrafast🚀🚀🚀.

Before we start to implement prerendering in angular application lets first see what is prerendering and how it is different from server-side rendering.

In short definition:

  • Server-Side Rendering (SSR):  rendering a client-side or universal app to HTML on the server.
  • Prerendering: running a client-side application at build time to capture its initial state as static HTML.

In both rendering technique, we generate static HTML pages which can be easily crawled by crawlers. Just the major difference is the location where HTML pages are generated.

In Server-Side Rendering Technique, when the user navigates to the URL => for example /books route, the server compiles the application(render on a server) and sends the generated HTML page back to the client.

While In Prerendering Technique, All routes of the application are compiled at build time and saved as static HTML pages on file-system. These HTML pages can be served to the client using CDN. So whenever the user navigates to any route, he/she will get prerendered HTML pages. 

Concept Visualization : Server Side Rendering and Prerendering
Concept Visualization : Server Side Rendering and Prerendering

This is the reason why the Prerendering technique is faster then Server-Side Rendering. 

Prerendering has the advantage of being able to serve requests stupidly fast, because nothing needs to be generated on the fly.

In fact, since your site’s responses are all generated ahead of time, you can store the files all over the world on a CDN (on the edge). This gives your site ridiculously fast response times.

Server-Side Rendering adds extra overhead for processing (compilation on the server).

With this advantage, Prerendering technique also has some drawbacks:

  • We need to generate a HTML for each possible route at build time. Which adds extra overhead in the build process. 
  • Prerendering doesn’t work for pages where we show content dynamically based on user role or some logic, because the user role will be available at the run time.
  • When page content changes frequently, Whenever content changes we need to rebuild the application and save the generated files in File System.

In scenarios, where we need to generate pages dynamically or where content changes required frequently, we should go with Server Side Rendering. 

Angular Universal gives us feature to implement both rendering technique.

For demonstrating angular application prerendering, I have created one sample angular application called Game Of Thrones Info. This application has the following routes:

  • /home
  • /books
  • /characters
  • /characters/:id
Initial App Setup
Initial App Setup

We will use Angular Universal to perform server-side rendering and prerendering.

Implement SSR and Prerendering along with this article. get the initial app setup at https://github.com/ngdevelop-tech/got-prerender-demo.git (checkout to the initial-setup branch)

To capture the initial loading time of our application we will use one of the important performance matric  FCP (First Contentful Paint).

FCP marks the time at which the first text or image is painted.

We will add the following script in index.html to measure FCP.

<script>
  // Log the value of FCP to the console | https://web.dev/fcp/#measure-fcp-in-javascript 
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntriesByName('first-contentful-paint')) {
      console.log('FCP: ', entry.startTime);
      observer.disconnect();
    }
  })

  observer.observe({type: 'paint', buffered: true});
</script>

We first measure the performance of our application without SSR and Prerendering. we can use this states for comparison after implementing SSR and Prerendering.

Our application performance looks like below:

Application Performance Before Implementing SSR and Prerendering
Application Performance Before Implementing SSR and Prerendering | FCP : ~890 ms

As you can see above, it takes nearly ~890 ms for first contentful paint, which is due to it consumes some time in scripting and compiling JS files.

Note: Performance time will vary with application to application, as you refresh the page and when cache is disabled/enabled.

Now let’s add server-side rendering and prerendering using Angular Universal.

We will use angular universal to implement Server-Side Rendering and Prerendering.

👉 Refer to this article for more details about Server-Side Rendering implementation in Angular application.

Execute the following angular universal schematic to implement server-side rendering.

ng add @nguniversal/express-engine

This will add and update the required files for Server Side Rendering and Prerendering.

👉 Angular Universal v9 added CLI Builders

  • For a LIVE-reloading Node & browser dev-server: which allows us to test the SSR changes at the time of development.
  • Prerender builder for prerendering. It uses guess-parser to guess the application routes.

Angular Universal ng add schematic will update package.json and angular.json as follows

  • Add scripts for SSR and prerendering in package.json and
  • Add CLI builders for live reloading and  prerendering in angular.json
{
    ...
    "newProjectRoot": "projects",
    "projects": {
       ...
      "got-prerender-demo": {
        "architect": {
            ...
            "serve-ssr": {
                "builder": "@nguniversal/builders:ssr-dev-server",
                "options": {
                "browserTarget": "got-prerender-demo:build",
                "serverTarget": "got-prerender-demo:server"
                },
                "configurations": {
                "production": {
                    "browserTarget": "got-prerender-demo:build:production",
                    "serverTarget": "got-prerender-demo:server:production"
                }
                }
            },
            "prerender": {
                "builder": "@nguniversal/builders:prerender",
                "options": {
                "browserTarget": "got-prerender-demo:build:production",
                "serverTarget": "got-prerender-demo:server:production",
                "routes": [
                    "/"
                ]
                },
                "configurations": {
                "production": {}
                }
            }
        }
      }},
    "defaultProject": "got-prerender-demo"
  }
{
    "name": "got-prerender-demo",
    "scripts": {
      ...
      "dev:ssr": "ng run got-prerender-demo:serve-ssr",
      "serve:ssr": "node dist/got-prerender-demo/server/main.js",
      "build:ssr": "ng build --prod && ng run got-prerender-demo:server:production",
      "prerender": "ng run got-prerender-demo:prerender"
    }
}

Great!!! We are done with default SSR and prerendering implementation ✨✨✨. Now Let’s measure SSR application performance📈.

Server Side Rendered Application Performance

Build SSR application and serve it using the following command

npm run build:ssr && npm run serve:ssr
Performance States after for Server Side Rendered Application
Performance States for Server Side Rendered Application | FCP : ~262 ms

As you can see above, it now nearly takes ~262 ms for first contentful paint. This is a great improvement in application loading time. Also, this application can be crawled using any crawler. 

Prerender Static Routes

To prerender static routes we just need to execute the following command. This will look for all static routes using guess-parser and prerender it.

npm run prerender
Prerender Angular Application using Angular Universal | Prerender Static Routes
Prerendering : npm run prerender
Prerender Angular Application using Angular Universal Prerenderer | Prerendered Static Routes

As you can see, it will generate all static routes of Game of thrones application. Prerendering also renders the data received from API response.

When we publish this application, You might get following question 🤔 

When the user navigates to any route (like /books), How Server/CDN knows, from where it needs to give a response? is it from a /books/index.html or from an angular application route?

The answer is nowadays CDN / Server is smart enough to detect the valid page response, when we navigate to a route like /books, if it finds folder like /books/index.html, it will render that prerendered page otherwise it will look for the route in angular app.  

Prerendering Dynamic / Parameterized Routes

Guess-parser is able to detect static routes, but it is unable to detect parameterized routes like /characters/:id, because how could it knows what are the -real – possible values of id !!!

Fortunately, Angular Universal has also added feature to manually assign the real possible values for route parameter ( like :id).

We can assign it using two ways :

👉 1st way:

We can manually provide routes using --routes flag for each route in the terminal.

ng run got-prerender-demo:prerender --routes "/characters/583" --routes "/characters/584"

or configuring each route in angular.json routes property of prerender builder configuration.

"prerender": {
  "builder": "@nguniversal/builders:prerender",
  "options": {
    "browserTarget": "got-prerender-demo:build:production",
    "serverTarget": "got-prerender-demo:server:production",
    "routes": [
      "/characters/583",
      "/characters/584"
    ]
  },
  "configurations": {
    "production": {}
  }
}

👉 2nd way:

If we have a large number of routes we can specify each route in a separate .txt file and assign it using --routesFile flag or prerender builder routesFile parameter as below.

For example, I have created dynamic-routes.txt in src folder, and provide all required additional routes.

/characters/1
/characters/2
/characters/3
/characters/583
/characters/584
/characters/585

We can use this routes file by assigning it with --routesFile flag as below

ng run prerender --routesFile 'dynamic-routes.txt'

or specify routesFile in angular.json prerender builder configuration.

"prerender": {
  "builder": "@nguniversal/builders:prerender",
  "options": {
    "browserTarget": "got-prerender-demo:build:production",
    "serverTarget": "got-prerender-demo:server:production",
    "routesFile": "./dynamic-routes.txt"
  },
  "configurations": {
    "production": {}
  }
}
Prerender Angular Application using Angular Universal | Prerendered Dynamic Routes
Prerendering bulk dynamic Routes with .txt file

Tip

👉 We only need to include additional routes that aren’t automatically detected by the guess-parser.

👉 For a large number of parameterized routes we can write a script to generate routes in .txt file 

Prerendered Application Performance

Once prerendered routes are generated we can test it’s performance by launching dist/[app_name]/browser folder on http-server or lite-server.

Prerender Angular Application using Angular Universal Prerenderer | Performance after prerendering
Performance after implementing prerendering | FCP ~200ms

As you can see, We receive first contentful paint now in ~200 ms.

Though in the local machine it is difficult to compare the loading time of each rendering technique, it gives some idea of how it will perform in production. 

Great !!! ✨✨✨ We are done with both Server Side Rendering and Prerendering Implementation.

Find the complete implementation of SSR and Prerendering at below repository

https://github.com/ngdevelop-tech/got-prerender-demo.git

Checkout to the initial-setup branch for application without SSR and Prerendering

Check out the Live Application at 🌍 https://got-prerender-demo.web.app/

In this article, we have seen

  • What is prerendering and server-side rendering? and How both are different?
  • How to measure application performance using FCP?
  • Implementing server-side rendering.
  • Prerender angular application using angular universal prerenderer.
  • Prerendering static routes and dynamic routes.

I hope you like this article, please provide your valuable feedback and suggestions in the comment section below🙂.

For more updates, Follow us 👍 on NgDevelop Facebook page.

Give your 🤎 with Like and Share.