Compare generated content against existing files before writing. This prevents unnecessary git diffs, cache invalidation, and downstream rebuilds.
Build scripts that generate HTML, JSON, XML, or other output files often write the result unconditionally, even when the content has not changed. This creates problems in CI pipelines: unchanged files get new timestamps, git status reports false modifications, auto-commit steps push empty diffs, and downstream caches are invalidated for no reason.
The Problem
A typical build script writes output directly:
import fs from 'node:fs';
const html = generatePage(data);
fs.writeFileSync('dist/index.html', html);
If generatePage produces the same HTML as the file already on disk, the write is wasted. Worse, it updates the file's modification timestamp, which can trigger:
git diffreporting the file as changed (if line endings or encoding differ)- Downstream tools rebuilding dependents of that file
- CI auto-commit steps creating empty or noise-only commits
- CDN or deployment cache invalidation for files that did not actually change
The Fix: Compare Before Writing
Read the existing file, compare it with the new content, and skip the write when they match:
import fs from 'node:fs';
import path from 'node:path';
function writeIfChanged(filePath, content) {
try {
const existing = fs.readFileSync(filePath, 'utf-8');
if (existing === content) return false;
} catch {
// File does not exist yet, proceed with write
}
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content);
return true;
}
Usage in a build script:
const pages = generateAllPages(data);
let written = 0;
for (const [filePath, html] of Object.entries(pages)) {
if (writeIfChanged(filePath, html)) {
written++;
}
}
console.log(`Wrote ${written}/${Object.keys(pages).length} files (rest unchanged)`);
On a typical run where 3 out of 300 pages changed, you will see:
Wrote 3/300 files (rest unchanged)
Apply to Every Generated Output
This pattern works for any generated file type:
// HTML pages
writeIfChanged('dist/tips.html', renderTipsPage(tips));
// JSON search indexes
writeIfChanged('public/search-index.json', JSON.stringify(index));
// XML feeds and sitemaps
writeIfChanged('public/feed.xml', renderRssFeed(posts));
writeIfChanged('public/sitemap.xml', renderSitemap(routes));
CI Auto-Commit: Only Commit When Files Actually Changed
When your workflow auto-commits generated files, the incremental write pattern prevents empty commits:
- name: Generate static assets
run: node scripts/build.js
- name: Commit generated files if changed
run: |
git add -A
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "chore: regenerate static assets"
git push
fi
Without incremental writes, git diff --cached would always detect changes because every file received a fresh write (and potentially a new timestamp or trailing whitespace difference), leading to commits that contain no meaningful content changes.
Key Points
- Always compare new content with existing files before writing in build scripts.
- Skipping unchanged writes prevents false
git diffresults, unnecessary cache invalidation, and empty auto-commits. - The
try/catchpattern handles missing files gracefully on first builds. - This optimization compounds in projects with hundreds of generated files, where only a few change per commit.
- Pair with
git diff --cached --quietin CI to avoid committing when nothing actually changed.
Related Tips
View all tips →Cache Playwright and Puppeteer Browsers in GitHub Actions
Cache browser binaries with actions/cache to avoid downloading 150MB+ on every CI run.
Skip Expensive CI Steps Using git diff Instead of File Timestamps
File modification timestamps are unreliable in GitHub Actions. Use git diff to detect actual changes and conditionally skip expensive workflow steps.