Skip to main content

Angular Prerendering

Introduction

Prerendering is a powerful technique that generates HTML files for your Angular application during the build process, rather than at runtime on the server or in the browser. It's a specific application of Server-Side Rendering (SSR) that happens at build time, resulting in static HTML files that can be served directly to users and search engines.

In this guide, we'll explore Angular prerendering, how it differs from traditional SSR, its benefits, and how to implement it in your Angular applications.

Understanding Prerendering

What is Prerendering?

Prerendering is the process of rendering your Angular application's pages to HTML during the build process. Unlike traditional Server-Side Rendering, which generates HTML on-demand when a user requests a page, prerendering happens just once - when you build your app.

Prerendering vs. Server-Side Rendering

Let's understand the key differences:

PrerenderingServer-Side Rendering
Occurs at build timeOccurs at request time
Outputs static HTML filesGenerates HTML dynamically per request
No runtime server neededRequires a running Node.js server
Best for content that rarely changesBetter for dynamic, frequently changing content
Simplest deploymentMore complex deployment

Benefits of Prerendering

  1. Improved SEO - Search engines can easily index your content as it's available in the initial HTML.
  2. Faster Initial Load - Users see content immediately without waiting for JavaScript to load and execute.
  3. Lower Server Costs - No need for a continuously running server to render pages.
  4. Better Performance - Static HTML files can be cached at CDN edges for even faster delivery.
  5. Simpler Deployment - Deploy to any static file hosting service without running a Node.js server.

Implementing Prerendering in Angular

Let's walk through how to add prerendering to your Angular application.

Prerequisites

Before we begin, ensure you have:

  • Angular project (version 9 or higher)
  • Angular CLI installed

Step 1: Add Angular Universal to your project

First, we need to add Angular Universal to enable server-side rendering capabilities:

bash
ng add @nguniversal/express-engine

This command will:

  • Add necessary dependencies
  • Create server-side specific files
  • Update your Angular configuration

Step 2: Configure Prerendering

Once Universal is set up, we need to modify the angular.json file to enable prerendering. Look for the architect section and add a prerender configuration:

json
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"routes": [
"/",
"/about",
"/products",
"/contact"
]
},
"configurations": {
"production": {
"browserTarget": "your-app-name:build:production",
"serverTarget": "your-app-name:server:production"
}
}
}

This configuration tells Angular which routes should be prerendered when building the application.

Step 3: Run the Prerendering Process

Now you can prerender your application with this command:

bash
ng run your-app-name:prerender

For a production build, use:

bash
ng run your-app-name:prerender:production

After running this command, you'll find the prerendered HTML files in the dist/your-app-name/browser directory. Each route specified in your configuration will have its own HTML file.

Dynamic Routes in Prerendering

What if your application has dynamic routes, like product pages with IDs from a database?

Example: Handling Dynamic Routes

You can set up your prerender configuration to handle dynamic routes by using a script that outputs all possible routes:

  1. Create a file called prerender-routes.js in your project root:
javascript
// prerender-routes.js
const fs = require('fs');

// This would typically come from a database or API call
const productIds = ['product1', 'product2', 'product3'];

// Generate all routes you want to prerender
const routes = [
'/',
'/about',
'/products',
'/contact',
...productIds.map(id => `/product/${id}`)
];

// Save routes to a file that Angular's prerender process can read
fs.writeFileSync('./prerender-routes.json', JSON.stringify(routes));

console.log(`Prerendering ${routes.length} routes`);
  1. Modify your angular.json to use this routes file:
json
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"routesFile": "./prerender-routes.json"
},
"configurations": {
"production": {
"browserTarget": "your-app-name:build:production",
"serverTarget": "your-app-name:server:production"
}
}
}
  1. Run your script before prerendering:
bash
node prerender-routes.js && ng run your-app-name:prerender

Real-world Example: Creating a Blog with Prerendering

Let's implement a simple blog application with prerendered pages:

Step 1: Create blog post component

typescript
// blog-post.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BlogService } from '../services/blog.service';

@Component({
selector: 'app-blog-post',
template: `
<article *ngIf="post">
<h1>{{post.title}}</h1>
<div class="metadata">Published: {{post.publishDate | date}}</div>
<div class="content" [innerHTML]="post.content"></div>
</article>
`
})
export class BlogPostComponent implements OnInit {
post: any;

constructor(
private route: ActivatedRoute,
private blogService: BlogService
) {}

ngOnInit() {
const slug = this.route.snapshot.paramMap.get('slug');
this.post = this.blogService.getPostBySlug(slug);
}
}

Step 2: Create a blog service

typescript
// blog.service.ts
import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root'
})
export class BlogService {
private posts = [
{
id: 1,
slug: 'getting-started-with-angular',
title: 'Getting Started with Angular',
publishDate: '2023-01-15',
content: '<p>Angular is a platform for building web applications...</p>'
},
{
id: 2,
slug: 'understanding-rxjs',
title: 'Understanding RxJS in Angular',
publishDate: '2023-02-20',
content: '<p>RxJS is a library for reactive programming...</p>'
},
// More posts...
];

getAllPosts() {
return this.posts;
}

getPostBySlug(slug: string) {
return this.posts.find(post => post.slug === slug);
}

getAllSlugs() {
return this.posts.map(post => post.slug);
}
}

Step 3: Set up routes

typescript
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { BlogListComponent } from './components/blog-list.component';
import { BlogPostComponent } from './components/blog-post.component';

const routes: Routes = [
{ path: '', component: BlogListComponent },
{ path: 'blog/:slug', component: BlogPostComponent }
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

Step 4: Generate prerender routes

javascript
// prerender-blog-routes.js
const fs = require('fs');

// In a real app, you might fetch this from an API or CMS
const blogPosts = [
{ slug: 'getting-started-with-angular' },
{ slug: 'understanding-rxjs' }
];

const routes = [
'/',
...blogPosts.map(post => `/blog/${post.slug}`)
];

fs.writeFileSync('./blog-routes.json', JSON.stringify(routes));
console.log(`Prerendering ${routes.length} routes for the blog`);

Step 5: Update your angular.json to use these routes

json
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"routesFile": "./blog-routes.json"
},
"configurations": {
"production": {
"browserTarget": "blog-app:build:production",
"serverTarget": "blog-app:server:production"
}
}
}

Best Practices for Prerendering

  1. Identify Static vs. Dynamic Content

    • Prerendering works best for content that doesn't change frequently.
    • Consider hybrid approaches for applications with both static and dynamic content.
  2. Handle Browser-Specific APIs

    • Use Angular's platform detection to avoid referencing window, document, or other browser-specific objects during prerendering:
typescript
import { isPlatformBrowser } from '@angular/common';
import { Component, Inject, PLATFORM_ID } from '@angular/core';

@Component({
selector: 'app-example',
template: '<div>Example Component</div>'
})
export class ExampleComponent {
constructor(@Inject(PLATFORM_ID) private platformId: Object) {
if (isPlatformBrowser(this.platformId)) {
// Execute browser-only code
console.log(window.innerWidth);
}
}
}
  1. Optimize for SEO
    • Add metadata to your components for proper SEO:
typescript
import { Component, OnInit } from '@angular/core';
import { Meta, Title } from '@angular/platform-browser';

@Component({
selector: 'app-product',
template: '...'
})
export class ProductComponent implements OnInit {
constructor(
private meta: Meta,
private titleService: Title
) {}

ngOnInit() {
this.titleService.setTitle('Product Name - Your Store');
this.meta.updateTag({ name: 'description', content: 'Product description goes here' });
this.meta.updateTag({ property: 'og:title', content: 'Product Name' });
// More meta tags...
}
}
  1. Consider Build Time
    • Prerendering can significantly increase build times for large applications.
    • Consider using selective prerendering for your most important pages.

Common Challenges and Solutions

1. Missing Data During Prerendering

Problem: Data from APIs might not be available during build-time rendering.

Solution: Use Angular's APP_INITIALIZER to prefetch data before your application renders:

typescript
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { DataService } from './services/data.service';

export function initializeApp(dataService: DataService) {
return () => dataService.loadInitialData();
}

@NgModule({
providers: [
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
deps: [DataService],
multi: true
}
]
})
export class AppModule { }

2. Third-party Libraries Using Browser APIs

Problem: Libraries that directly access browser APIs can cause prerendering to fail.

Solution: Use dynamic imports for browser-specific code:

typescript
// In your component
import { Component, OnInit } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { PLATFORM_ID, Inject } from '@angular/core';

@Component({
selector: 'app-chart',
template: '<div id="chart-container"></div>'
})
export class ChartComponent implements OnInit {
constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

async ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
// Dynamically import the chart library only in the browser
const ChartModule = await import('chart.js');
const Chart = ChartModule.default;

const ctx = document.getElementById('chart-container');
new Chart(ctx, {
// Chart configuration...
});
}
}
}

Summary

Angular prerendering is a powerful technique that combines the benefits of server-side rendering with the simplicity of static site generation. It provides excellent performance and SEO benefits by generating HTML content at build time, making it perfect for content-focused applications.

In this guide, we covered:

  • The concept and benefits of prerendering
  • How to implement prerendering in your Angular applications
  • Handling dynamic routes with prerendering
  • A real-world blog example using prerendering
  • Best practices and common challenges with solutions

Prerendering is an excellent middle ground between pure client-side rendering and full server-side rendering, offering many of the benefits of SSR without the ongoing server costs and complexity.

Additional Resources

Practice Exercises

  1. Convert an existing Angular application to use prerendering for its main routes.
  2. Build a simple portfolio website with Angular that uses prerendering for project pages.
  3. Create a prerendering script that pulls content from a headless CMS and generates routes dynamically.
  4. Implement a hybrid approach that uses prerendering for static content and client-side rendering for dynamic features.
  5. Compare the performance of your application before and after implementing prerendering using Lighthouse or similar tools.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)