示例说明
全量重建上千页面的静态站耗时以分钟计,而多数提交只改动少数几页。以页面源内容为输入计算哈希,manifest 记录每页上次构建的指纹:本次哈希相同直接跳过,不同才重新渲染。构建时间从页面总量缩到变更量,缓存产物也可以直接推 CDN 复用。
技术要点
- 内容哈希作为缓存键天然自失效
- manifest 持久化跨次构建的指纹
- 仅变更页面进入渲染路径
代码解读
buildIncremental 读取页面目录与上次 manifest,逐页计算 sha256 截断摘要;命中即跳过渲染并把指纹延续到新 manifest,未命中执行简单的标题转换渲染并写缓存目录。最后统计重建数与缓存命中数,返回值直接可用于构建日志。
使用建议
模板或全局组件变更时应让所有页面失效,把模板版本并入哈希输入。清理缓存目录遵循产物可重建原则,损坏就整体丢弃。CI 里缓存目录配合键控还原效果最佳。
代码示例
// SSG build cache: invalidate prerender output when page hashes change
import { createHash } from 'node:crypto';
import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises';
async function buildIncremental(pagesDir, cacheDir) {
await mkdir(cacheDir, { recursive: true });
const pages = (await readdir(pagesDir)).filter((f) => f.endsWith('.md'));
let cacheMeta = {};
try {
cacheMeta = JSON.parse(await readFile(cacheDir + '/manifest.json', 'utf8'));
} catch {
cacheMeta = {};
}
const manifest = {};
let rebuilt = 0;
for (const file of pages) {
const raw = await readFile(pagesDir + '/' + file, 'utf8');
const hash = createHash('sha256').update(raw).digest('hex').slice(0, 16);
if (cacheMeta[file] === hash) {
manifest[file] = hash; // 静态内容未变,复用缓存产物
continue;
}
const html = '<article>' + raw.replace(/^# (.*)$/m, '<h1>$1</h1>') + '</article>';
await writeFile(cacheDir + '/' + file.replace(/\.md$/, '.html'), html);
manifest[file] = hash;
rebuilt += 1;
}
await writeFile(cacheDir + '/manifest.json', JSON.stringify(manifest, null, 2));
return { total: pages.length, rebuilt, cached: pages.length - rebuilt };
}
const stats = await buildIncremental('content/pages', 'dist/cache');
console.log('build finished:', stats);