angular

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

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

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 | 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 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
Prerendering : npm run prerender

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": {}
  }
}
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.

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.

Ankit

Disqus Comments Loading...

Recent Posts

Awesome Charts in Angular 13 with ng2-charts

Charts help us to visualize large amounts of data in an easy-to-understand and interactive way.  …

2 years ago

Client-Side PDF Generation In Angular 13 With PDFMake

Generating reports, invoices, blog content, documents, guidelines, forms, etc in PDF form is a common…

2 years ago

Dynamically Add Title and Meta Tags on Route Change in Angular

Web Page title, description, and other meta tags are very important for the Search Engine…

4 years ago

Integrate Google Analytics with Angular | Angular SEO

Google Analytics is a popular analytical tool for web and mobile applications from Google. It…

4 years ago

16 Best Chrome DevTools Tips and Tricks

Chrome DevTools provides amazing features to debug source code, capture element state, update and test…

4 years ago

Data Visualization with ECharts in Angular using ngx-echarts

Angular Charts Series Articles Best Angular Chart Libraries How to use ngx-charts in angular application…

4 years ago