← 中文资源

简体中文版本

SSG 构建缓存增量失效

按内容哈希实现静态站点的增量构建:每页输入算摘要,未变化的复用缓存产物,只有内容变动的页面重新生成。

阅读英文原文

示例说明

全量重建上千页面的静态站耗时以分钟计,而多数提交只改动少数几页。以页面源内容为输入计算哈希,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);