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:
Prerendering | Server-Side Rendering |
---|---|
Occurs at build time | Occurs at request time |
Outputs static HTML files | Generates HTML dynamically per request |
No runtime server needed | Requires a running Node.js server |
Best for content that rarely changes | Better for dynamic, frequently changing content |
Simplest deployment | More complex deployment |
Benefits of Prerendering
- Improved SEO - Search engines can easily index your content as it's available in the initial HTML.
- Faster Initial Load - Users see content immediately without waiting for JavaScript to load and execute.
- Lower Server Costs - No need for a continuously running server to render pages.
- Better Performance - Static HTML files can be cached at CDN edges for even faster delivery.
- 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:
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:
"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:
ng run your-app-name:prerender
For a production build, use:
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:
- Create a file called
prerender-routes.js
in your project root:
// 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`);
- Modify your
angular.json
to use this routes file:
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"routesFile": "./prerender-routes.json"
},
"configurations": {
"production": {
"browserTarget": "your-app-name:build:production",
"serverTarget": "your-app-name:server:production"
}
}
}
- Run your script before prerendering:
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
// 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
// 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
// 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
// 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
"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
-
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.
-
Handle Browser-Specific APIs
- Use Angular's platform detection to avoid referencing
window
,document
, or other browser-specific objects during prerendering:
- Use Angular's platform detection to avoid referencing
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);
}
}
}
- Optimize for SEO
- Add metadata to your components for proper SEO:
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...
}
}
- 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:
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:
// 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
- Angular Universal Documentation
- Angular Prerendering API
- Scully - Static Site Generator for Angular
- Jamstack Architecture
Practice Exercises
- Convert an existing Angular application to use prerendering for its main routes.
- Build a simple portfolio website with Angular that uses prerendering for project pages.
- Create a prerendering script that pulls content from a headless CMS and generates routes dynamically.
- Implement a hybrid approach that uses prerendering for static content and client-side rendering for dynamic features.
- 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! :)