Pre-rendering / SSG
Generate static HTML at build time for instant page loads and better SEO.
Pre-rendering / SSG (Static Site Generation)
Problem
Client-side rendering shows blank pages while JavaScript loads and executes. This hurts FCP, LCP, SEO, and user experience. On slow networks, users wait 5+ seconds for content.
Solution
Pre-render pages to static HTML at build time. Users get instant content, search engines can crawl, and JavaScript hydrates interactivity progressively.
/**
* Static page generator
*/
interface PageData {
path: string;
title: string;
content: string;
meta: Record<string, string>;
}
interface SSGConfig {
pages: PageData[];
outputDir: string;
template: string;
}
async function generateStaticSite(config: SSGConfig): Promise<void> {
const fs = await import('fs/promises');
const path = await import('path');
for (const page of config.pages) {
const html = renderPage(page, config.template);
const filePath = path.join(config.outputDir, page.path, 'index.html');
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, html, 'utf-8');
console.log(`Generated: ${filePath}`);
}
}
function renderPage(page: PageData, template: string): string {
return template
.replace('{{title}}', page.title)
.replace('{{content}}', page.content)
.replace('{{meta}}', generateMetaTags(page.meta));
}
function generateMetaTags(meta: Record<string, string>): string {
return Object.entries(meta)
.map(([key, value]) => `<meta property="${key}" content="${value}">`)
.join('\n');
}
/**
* Data fetching at build time
*/
interface Post {
id: string;
title: string;
content: string;
slug: string;
}
async function getStaticPaths(): Promise<string[]> {
// Fetch all post slugs at build time
const response = await fetch('https://api.example.com/posts');
const posts = await response.json() as Post[];
return posts.map((post) => `/posts/${post.slug}`);
}
async function getStaticProps(slug: string): Promise<Post> {
// Fetch post data at build time
const response = await fetch(`https://api.example.com/posts/${slug}`);
return response.json() as Promise<Post>;
}
/**
* Incremental Static Regeneration (ISR)
*/
interface ISRConfig {
revalidate: number; // Seconds
fallback: 'blocking' | boolean;
}
class ISRManager {
private cache = new Map<string, { data: unknown; timestamp: number }>();
constructor(private config: ISRConfig) {}
/**
* Get data with revalidation
*/
public async getData<T>(
key: string,
fetcher: () => Promise<T>
): Promise<T> {
const cached = this.cache.get(key);
if (cached) {
const age = Date.now() - cached.timestamp;
if (age < this.config.revalidate * 1000) {
// Fresh cache
return cached.data as T;
}
// Stale cache - revalidate in background
this.revalidate(key, fetcher);
return cached.data as T;
}
// No cache - fetch now
const data = await fetcher();
this.cache.set(key, { data, timestamp: Date.now() });
return data;
}
private async revalidate<T>(key: string, fetcher: () => Promise<T>): Promise<void> {
try {
const data = await fetcher();
this.cache.set(key, { data, timestamp: Date.now() });
} catch (error) {
console.error(`Revalidation failed for ${key}:`, error);
}
}
}
/**
* Hybrid rendering strategy
*/
enum RenderStrategy {
SSG = 'ssg', // Static at build time
ISR = 'isr', // Static with revalidation
SSR = 'ssr', // Server-side on request
CSR = 'csr', // Client-side only
}
interface RouteConfig {
path: string;
strategy: RenderStrategy;
revalidate?: number;
}
const routes: RouteConfig[] = [
{ path: '/', strategy: RenderStrategy.SSG },
{ path: '/about', strategy: RenderStrategy.SSG },
{ path: '/blog/:slug', strategy: RenderStrategy.ISR, revalidate: 60 },
{ path: '/dashboard', strategy: RenderStrategy.SSR },
{ path: '/realtime', strategy: RenderStrategy.CSR },
];Next.js Implementation
// pages/blog/[slug].tsx
import { GetStaticPaths, GetStaticProps } from 'next';
interface PostProps {
post: Post;
}
export default function BlogPost({ post }: PostProps) {
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// Generate paths at build time
export const getStaticPaths: GetStaticPaths = async () => {
const posts = await fetchAllPosts();
return {
paths: posts.map((post) => ({
params: { slug: post.slug },
})),
fallback: 'blocking', // Generate on-demand for new posts
};
};
// Fetch data at build time
export const getStaticProps: GetStaticProps<PostProps> = async ({ params }) => {
const post = await fetchPost(params?.slug as string);
return {
props: { post },
revalidate: 60, // ISR: Regenerate after 60 seconds
};
};Build Script
// build.ts
async function buildSite(): Promise<void> {
console.log('Building static site...');
// 1. Fetch all data
const posts = await fetchAllPosts();
const pages = await fetchAllPages();
// 2. Generate static pages
await generateStaticSite({
pages: [
...pages.map(pageToData),
...posts.map(postToData),
],
outputDir: './dist',
template: await readTemplate('./template.html'),
});
// 3. Copy assets
await copyAssets('./public', './dist');
// 4. Generate sitemap
await generateSitemap([...pages, ...posts], './dist/sitemap.xml');
console.log('Build complete!');
}
async function readTemplate(path: string): Promise<string> {
const fs = await import('fs/promises');
return fs.readFile(path, 'utf-8');
}
async function copyAssets(source: string, dest: string): Promise<void> {
const fs = await import('fs/promises');
await fs.cp(source, dest, { recursive: true });
}
function pageToData(page: unknown): PageData {
// Transform page to PageData
return page as PageData;
}
function postToData(post: Post): PageData {
return {
path: `/blog/${post.slug}`,
title: post.title,
content: post.content,
meta: {
'og:title': post.title,
'og:type': 'article',
},
};
}Sitemap Generation
function generateSitemap(pages: Array<{ path: string }>, output: string): Promise<void> {
const fs = require('fs/promises');
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages.map((page) => `
<url>
<loc>https://example.com${page.path}</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
`).join('')}
</urlset>`;
return fs.writeFile(output, sitemap);
}Performance Comparison
Client-Side Rendering (CSR)
- TTFB: 200ms
- FCP: 3.2s (wait for JS)
- LCP: 4.5s
- TTI: 5.0s
- SEO: Poor (crawlers see blank page)
Static Site Generation (SSG)
- TTFB: 50ms
- FCP: 0.8s (60% faster)
- LCP: 1.2s (73% faster)
- TTI: 2.0s (60% faster)
- SEO: Excellent (full HTML)
3-4x faster than CSR
When to Use Each Strategy
SSG (Static Site Generation)
✅ Use for:
- Marketing pages
- Blog posts
- Documentation
- Landing pages
- Product catalogs
Benefits: Fastest, best SEO, cheap hosting
ISR (Incremental Static Regeneration)
✅ Use for:
- Content that updates occasionally
- Product pages (price changes)
- News articles
- User profiles (public)
Benefits: Fast + fresh content
SSR (Server-Side Rendering)
✅ Use for:
- Personalized content
- Real-time data
- User dashboards
- Search results
Benefits: Fresh data per request
CSR (Client-Side Rendering)
✅ Use for:
- Highly interactive apps
- Real-time collaboration
- Admin panels
Benefits: Rich interactivity
Hybrid Approach
// Example: E-commerce site
const strategy = {
'/': 'SSG', // Homepage
'/products': 'ISR', // Product listing (revalidate hourly)
'/products/:id': 'ISR', // Product details (revalidate daily)
'/cart': 'CSR', // Shopping cart (client-only)
'/checkout': 'SSR', // Checkout (personalized)
'/account': 'SSR', // User account (authenticated)
};Impact at Scale
For a blog with 10,000 posts, 1M visitors/month:
CSR
- Server: Serves SPA shell (1 instance)
- API calls: 1M × 10 = 10M requests
- Load time: 3-5s average
SSG
- Server: Serves static HTML (CDN)
- API calls: 10,000 at build time (1 hour)
- Load time: 0.5-1s average
Cost savings: 90% less server load Performance gain: 3-5x faster SEO: 100% better indexing
Best Practices
- SSG by default: Pre-render everything possible
- ISR for dynamic: Content that changes occasionally
- SSR for auth: Personalized/sensitive content
- CSR for realtime: Chat, collaboration tools
- Hybrid approach: Mix strategies per route
- CDN distribution: Static files on edge
- Incremental builds: Only rebuild changed pages
Pre-rendering is mandatory for content-heavy sites. It's the foundation of modern web performance.