Incremental Builds — Building Only Changed Pages for Faster SSG
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
- Not tracking content dependencies. A blog listing page depends on all posts. If one post changes, the listing should rebuild. Track dependency graphs.
- Relying on file modification timestamps. Git clones reset timestamps. Use content hashing (MD5/SHA) for reliable change detection across environments.
- Forgetting to invalidate CDN cache for changed files. Incremental builds update files locally, but CDN caches may serve stale versions. Use cache invalidation APIs.
- Not handling asset changes. CSS, JavaScript, and image changes affect all pages. Incremental asset builds must trigger full page cache invalidation.
- Skipping listing/taxonomy updates. When a post changes, also rebuild category pages, tag pages, and the homepage. These depend on post data.
Practice Questions
- How does an incremental build differ from a full build?
- What is the best way to detect content changes between builds?
- How do you handle page dependencies in incremental builds?
- How does Next.js ISR differ from traditional incremental build approaches?
- 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
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