Front-end Engineering Lab

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

  1. SSG by default: Pre-render everything possible
  2. ISR for dynamic: Content that changes occasionally
  3. SSR for auth: Personalized/sensitive content
  4. CSR for realtime: Chat, collaboration tools
  5. Hybrid approach: Mix strategies per route
  6. CDN distribution: Static files on edge
  7. Incremental builds: Only rebuild changed pages

Pre-rendering is mandatory for content-heavy sites. It's the foundation of modern web performance.

On this page