Skip to content

Incremental Builds — Building Only Changed Pages for Faster SSG

DodaTech Updated 2026-06-28 5 min read

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

Incremental builds only regenerate pages that changed since the last build, dramatically reducing build time for large static sites.

What You'll Learn

By the end of this tutorial, you'll understand how incremental builds work, how to implement them in different SSGs, how to track content changes, and how to integrate incremental builds into CI/CD pipelines.

Why It Matters

Full rebuilds become impractical as sites grow. A 10,000-page site taking 30 minutes per build blocks content updates. Incremental builds reduce this to seconds by rebuilding only the 2-3 pages that actually changed.

Real-World Use

A news site with 50,000 articles publishes 20 new stories per hour. Instead of a 2-hour full rebuild, incremental builds Process only 20 new pages plus the homepage. Build completes in under 30 seconds.

Incremental Build Flow

graph TD
    A[Content Change] --> B[Detect changed files]
    B --> C{Which pages
are affected?} C --> D[New pages] C --> E[Updated pages] C --> F[Related pages
(homepage, listings)] D --> G[Render only
affected pages] E --> G F --> G G --> H[Update CDN cache
for changed files] H --> I[Site updated
in seconds] style B fill:#4a90d9,color:#fff style G fill:#e67e22,color:#fff style I fill:#27ae60,color:#fff

Custom Incremental Build Script

// scripts/incremental-build.js — Manual incremental build
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

const STATE_FILE = './.build-state.json';

// Load previous build state
function loadState() {
    if (fs.existsSync(STATE_FILE)) {
        return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
    }
    return { files: {}, buildTime: 0 };
}

// Save current build state
function saveState(state) {
    fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}

// Compute file hash
function fileHash(filePath) {
    const content = fs.readFileSync(filePath);
    return crypto.createHash('md5').update(content).digest('hex');
}

// Find changed files
function findChangedFiles(contentDir) {
    const previousState = loadState();
    const changedFiles = [];
    const newState = { files: {}, buildTime: Date.now() };

    function scan(dir) {
        const entries = fs.readdirSync(dir, { withFileTypes: true });
        for (const entry of entries) {
            const fullPath = path.join(dir, entry.name);

            if (entry.isDirectory() && entry.name !== 'node_modules') {
                scan(fullPath);
            } else if (entry.isFile() && entry.name.endsWith('.md')) {
                const hash = fileHash(fullPath);
                newState.files[fullPath] = hash;

                if (previousState.files[fullPath] !== hash) {
                    changedFiles.push(fullPath);
                }
            }
        }
    }

    scan(contentDir);
    saveState(newState);

    return changedFiles;
}

// Rebuild only changed pages
async function incrementalBuild() {
    const start = Date.now();
    console.log('Checking for changed content...');

    const changedFiles = findChangedFiles('./content');
    console.log(`Found ${changedFiles.length} changed files`);

    if (changedFiles.length === 0) {
        console.log('No changes detected. Skipping build.');
        return;
    }

    // Rebuild only changed pages
    for (const file of changedFiles) {
        const slug = path.basename(file, '.md');
        console.log(`  Rebuilding: ${slug}`);
        // Render and write the page
        await renderPage(slug);
    }

    // Also rebuild listing pages (homepage, categories, tags)
    await rebuildListings();

    console.log(`Incremental build completed in ${Date.now() - start}ms`);
}

Gatsby Incremental Builds

// gatsby-config.js — Gatsby Cloud incremental builds
module.exports = {
    flags: {
        DEV_SSR: false,
        FAST_DEV: true,
        PRESERVE_FILE_DOWNLOAD_CACHE: true,
        PRESERVE_WEBPACK_CACHE: true,
    },
    plugins: [
        {
            resolve: 'gatsby-source-contentful',
            options: {
                spaceId: process.env.CONTENTFUL_SPACE_ID,
                accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
                // Enable Contentful webhook-based incremental builds
                enableAdapter: true,
            },
        },
        {
            resolve: 'gatsby-plugin-incremental',
            options: {
                // Cache directory for incremental build data
                cacheDirectory: '.cache/incremental',
            },
        },
    ],
};

Next.js ISR vs Incremental Static Generation

// Next.js uses ISR, not traditional incremental builds.
// But you can optimize with on-demand revalidation:

// pages/api/revalidate.js — On-demand revalidation
export default async function handler(req, res) {
    const { secret, paths } = req.body;

    if (secret !== process.env.REVALIDATION_TOKEN) {
        return res.status(401).json({ message: 'Invalid token' });
    }

    // Only revalidate specific paths
    const results = await Promise.allSettled(
        paths.map(path => res.revalidate(path))
    );

    const succeeded = results.filter(r => r.status === 'fulfilled').length;
    const failed = results.filter(r => r.status === 'rejected').length;

    console.log(`Revalidated: ${succeeded} succeeded, ${failed} failed`);

    return res.json({
        revalidated: true,
        succeeded,
        failed,
        errors: results
            .filter(r => r.status === 'rejected')
            .map(r => r.reason.message),
    });
}

// Trigger revalidation from CMS webhook
// POST /api/revalidate
// Body: { secret: "xxx", paths: ["/blog/new-post", "/blog"] }

Eleventy Incremental Builds

// .eleventy.js — Incremental builds in Eleventy 2.0+
module.exports = function (eleventyConfig) {
    // Enable incremental builds
    eleventyConfig.setIncrementalBuilds(true);

    // Watch additional files
    eleventyConfig.addWatchTarget('./src/_data/');
    eleventyConfig.addWatchTarget('./src/assets/');

    // Passthrough copy
    eleventyConfig.addPassthroughCopy('src/css');
    eleventyConfig.addPassthroughCopy('src/images');

    return {
        dir: {
            input: 'src',
            output: '_site',
            includes: '_includes',
            layouts: '_layouts',
        },
        incremental: true,
    };
};

// Run with:
// npx @11ty/eleventy --incremental
// Or for development:
// npx @11ty/eleventy --serve --incremental

Common Mistakes

  1. Not tracking content dependencies. A blog listing page depends on all posts. If one post changes, the listing should rebuild. Track dependency graphs.
  2. Relying on file modification timestamps. Git clones reset timestamps. Use content hashing (MD5/SHA) for reliable change detection across environments.
  3. Forgetting to invalidate CDN cache for changed files. Incremental builds update files locally, but CDN caches may serve stale versions. Use cache invalidation APIs.
  4. Not handling asset changes. CSS, JavaScript, and image changes affect all pages. Incremental asset builds must trigger full page cache invalidation.
  5. Skipping listing/taxonomy updates. When a post changes, also rebuild category pages, tag pages, and the homepage. These depend on post data.

Practice Questions

  1. How does an incremental build differ from a full build?
  2. What is the best way to detect content changes between builds?
  3. How do you handle page dependencies in incremental builds?
  4. How does Next.js ISR differ from traditional incremental build approaches?
  5. Why should you invalidate CDN cache after incremental builds?

Challenge: Implement an incremental build system: track file hashes for all content, detect changes since last build, generate only modified pages and their dependents, and benchmark the speed improvement over a full build for a 100-page site.

FAQ

Do incremental builds work with all SSGs?

Most modern SSGs support incremental builds. Gatsby (Gatsby Cloud), Eleventy (--incremental), and Next.js (ISR) have built-in support. Hugo supports it via --renderStaticToDisk.

Can I use incremental builds without a cloud service?

Yes. Implement a custom hash-based change detection system. Track file hashes in a JSON state file and re-render only changed pages.

How do incremental builds handle deleted pages?

Deletion requires special handling. Detect removed files by comparing current file list with previous state, and remove their output files from the build directory.

Does incremental mode work during development?

Yes. Development servers use a form of incremental builds by default (hot reload). Changes to a single file reprocess only that file.

What happens to global data when using incremental builds?

Changes to global data files (_data/, site config) should trigger a full rebuild of all pages since all pages depend on global data.

Mini Project

Build an incremental build system for a 500-page site: implement content hash tracking, detect changed files between builds, render only affected pages, rebuild dependent pages (listings, categories), invalidate CDN cache for changed files, and measure build time reduction.

What's Next

Your builds are fast and incremental. Now learn about SSG Deployment to deploy your static site to CDNs and hosting platforms.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro