Skip to content

SSG Build Performance — Optimizing Build Time for Large Static Sites

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about SSG Build Performance. We cover key concepts, practical examples, and best practices to help you master this topic.

SSG build performance optimization focuses on incremental builds, Caching, parallel processing, content filtering, and efficient data fetching to keep builds fast.

What You'll Learn

By the end of this tutorial, you'll understand what makes SSG builds slow, how to implement incremental builds, cache data fetching, parallelize processing, and monitor build performance.

Why It Matters

A site that takes 2 hours to build can't iterate quickly. Content updates take forever, developers lose productivity, and deployment pipelines bottleneck. Optimizing build time is essential for scaling SSG.

Real-World Use

A documentation site grew from 100 to 15,000 pages. Build time went from 30 seconds to 45 minutes. After implementing incremental builds, data caching, and parallel processing, build time dropped back to under 5 minutes.

Build Performance Bottlenecks

graph TD
    A[Build Process] --> B[Data Fetching]
    A --> C[Template Rendering]
    A --> D[Asset Processing]
    A --> E[File Writing]
    B --> F[External API calls
Content fetching] C --> G[HTML generation
Page rendering] D --> H[Image optimization
CSS/JS bundling] E --> I[Output file writing] F --> J[Slowdown: API latency
Rate limits, large payloads] G --> K[Slowdown: Many pages
Complex templates] H --> L[Slowdown: Large images
Many variants] I --> M[Slowdown: Many small files] style J fill:#e74c3c,color:#fff style K fill:#e74c3c,color:#fff style L fill:#e74c3c,color:#fff style M fill:#e74c3c,color:#fff

Data Caching

// scripts/build-with-cache.js — Cache API responses
const fs = require('fs');
const path = require('path');

const CACHE_DIR = './.build-cache';

function getCacheKey(url) {
    return path.join(CACHE_DIR, Buffer.from(url).toString('base64'));
}

async function fetchWithCache(url, ttl = 3600000) {
    const cacheKey = getCacheKey(url);

    // Check cache
    if (fs.existsSync(cacheKey)) {
        const cached = JSON.parse(fs.readFileSync(cacheKey, 'utf8'));
        if (Date.now() - cached.timestamp < ttl) {
            console.log(`  Cache HIT: ${url}`);
            return cached.data;
        }
        console.log(`  Cache STALE: ${url}`);
    }

    // Fetch fresh data
    console.log(`  Cache MISS: ${url}`);
    const response = await fetch(url);
    const data = await response.json();

    // Store in cache
    if (!fs.existsSync(CACHE_DIR)) {
        fs.mkdirSync(CACHE_DIR, { recursive: true });
    }
    fs.writeFileSync(cacheKey, JSON.stringify({
        timestamp: Date.now(),
        data,
    }));

    return data;
}

// Usage in build
async function buildSite() {
    const start = Date.now();

    // Posts API - cached for 1 hour during development
    const posts = await fetchWithCache('https://api.example.com/posts', 3600000);

    // Categories API - cached for 6 hours
    const categories = await fetchWithCache(
        'https://api.example.com/categories',
        21600000
    );

    console.log(`Build completed in ${(Date.now() - start) / 1000}s`);
}

Parallel Processing

// scripts/build-parallel.js — Parallel page generation
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');

const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
const CONCURRENCY = 10; // Max parallel operations

// Queue-based parallel executor
async function parallelMap(items, fn, concurrency = CONCURRENCY) {
    const results = [];
    const queue = [...items];

    async function worker() {
        while (queue.length > 0) {
            const item = queue.shift();
            results.push(await fn(item));
        }
    }

    const workers = Array.from({ length: concurrency }, worker);
    await Promise.all(workers);
    return results;
}

// Generate pages in parallel
async function generateAllPages(posts) {
    const tasks = posts.map(post => async () => {
        const html = await renderPage(post);
        const outputPath = path.join('./public', `${post.slug}.html`);

        await writeFile(outputPath, html);
        return post.slug;
    });

    const generated = await parallelMap(posts, async (post) => {
        const html = await renderPage(post);
        const outputPath = path.join('./public', `${post.slug}.html`);
        await writeFile(outputPath, html);
        return post.slug;
    });

    console.log(`Generated ${generated.length} pages in parallel`);
    return generated;
}

// Batch image processing
async function processImagesInParallel(images) {
    const results = await parallelMap(images, async (imagePath) => {
        const output = await optimizeImage(imagePath);
        return output;
    }, 4); // Limit image processing to 4 concurrent

    return results;
}

Content Filtering

// gatsby-node.js — Skip draft and future content
exports.createPages = async ({ graphql, actions }) => {
    const { createPage } = actions;

    // Only fetch published, non-draft content
    const result = await graphql(`
        query {
            allMarkdownRemark(
                filter: {
                    frontmatter: {
                        draft: { ne: true }
                        date: { lte: "now" }
                    }
                }
                sort: { frontmatter: { date: DESC } }
            ) {
                edges {
                    node {
                        id
                        frontmatter {
                            slug
                            title
                            date
                        }
                    }
                }
            }
        }
    `);

    // Create pages only for filtered content
    result.data.allMarkdownRemark.edges.forEach(({ node }) => {
        createPage({
            path: `/blog/${node.frontmatter.slug}`,
            component: path.resolve('./src/templates/blog-post.js'),
            context: { id: node.id },
        });
    });
};

Build Monitoring

// scripts/build-monitor.js — Track build performance
class BuildMonitor {
    constructor() {
        this.marks = {};
        this.measures = [];
        this.startTime = Date.now();
    }

    mark(name) {
        this.marks[name] = Date.now();
        console.log(`[BUILD] ${name}: ${this.marks[name]}`);
    }

    measure(name, startMark, endMark) {
        const start = this.marks[startMark];
        const end = this.marks[endMark] || Date.now();
        const duration = end - start;

        this.measures.push({ name, duration });
        console.log(`[PERF] ${name}: ${(duration / 1000).toFixed(2)}s`);
    }

    summary() {
        const totalTime = Date.now() - this.startTime;

        console.log('\n=== Build Performance Summary ===');
        this.measures.forEach(m => {
            const pct = ((m.duration / totalTime) * 100).toFixed(1);
            console.log(`  ${m.name}: ${(m.duration / 1000).toFixed(2)}s (${pct}%)`);
        });
        console.log(`  TOTAL: ${(totalTime / 1000).toFixed(2)}s`);
        console.log('================================\n');
    }
}

// Usage in build script
const monitor = new BuildMonitor();

monitor.mark('fetch-start');
const data = await fetchData();
monitor.mark('fetch-end');
monitor.measure('Data fetching', 'fetch-start', 'fetch-end');

monitor.mark('render-start');
const pages = await renderPages(data);
monitor.mark('render-end');
monitor.measure('Page rendering', 'render-start', 'render-end');

monitor.mark('image-start');
await processImages();
monitor.mark('image-end');
monitor.measure('Image processing', 'image-start', 'image-end');

monitor.summary();

Common Mistakes

  1. Not caching API responses in development. Every dev server restart re-fetches all data. Cache to local files with a TTL to speed up development builds.
  2. Generating all pages in sequence. Page rendering is CPU-bound but data fetching is I/O-bound. Parallelize both independently.
  3. Processing all images at once without concurrency limits. Too many parallel Sharp operations can exhaust memory. Limit concurrent image processing to 4-8.
  4. Not filtering unpublished content. Drafts, future-dated posts, and hidden pages still consume build time. Filter them out early.
  5. Ignoring template complexity. Nested partials, heavy data queries, and complex loops slow rendering. Profile templates and simplify hot paths.

Practice Questions

  1. What are the three main bottlenecks in SSG build performance?
  2. How does API response caching speed up development builds?
  3. How does parallel page generation reduce total build time?
  4. Why should you filter draft and unpublished content during build?
  5. How do you measure and monitor build performance over time?

Challenge: Profile and optimize a slow SSG build: instrument the build Process with timing marks, identify the slowest phase, implement parallel processing and caching, and reduce total build time by at least 50%.

FAQ

How much can incremental builds reduce build time?

Incremental builds can reduce build time by 80-95% by only rebuilding changed pages. The actual gain depends on how many pages change per build.

What is the fastest SSG for large sites?

Hugo (Go) is the fastest, building 15,000 pages in ~30 seconds. Next.js and Gatsby are slower but offer richer features. Choose based on feature needs.

Should I use a build service or build locally?

Use CI/CD build services (Netlify, Vercel, Gatsby Cloud) for production. They offer caching, parallel builds, and incremental features not available locally.

How does content size affect build time?

Linearly in most SSGs. 2x content = 2x build time. Data fetching and image processing often scale worse than rendering due to API limits.

Can I build only changed pages in development?

Yes. Most SSGs support incremental development builds. Use --incremental flag in Eleventy, or next dev for Next.js which handles this automatically.

Mini Project

Optimize a slow SSG build: create a build monitoring dashboard that times each phase, implement data caching with configurable TTLs, add parallel page generation with concurrency limits, filter draft content from the build, and benchmark before/after improvements.

What's Next

Build is fast. Now implement Incremental Builds to rebuild only changed pages for even faster iteration.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro