增量静态生成实战:提升网站构建效率与用户体验

子尧的笔记 优化 阅读 922
赞 19 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

上个月我们上线了一个内容型站点,用的是 Next.js 的静态生成(SSG)。初期几百个页面,构建飞快,部署也稳。但随着内容量涨到 3000+ 页面,每次构建直接干到 15 分钟以上,CI/CD 流水线经常超时,开发体验差到爆——改一个文案,等十几分钟才能预览,谁受得了?

增量静态生成实战:提升网站构建效率与用户体验

更糟的是,就算只改了其中一篇博客,整个站点都得重新构建。用户访问新内容要等全站部署完,根本谈不上“增量更新”。团队里有人开始嘀咕:“是不是该换动态渲染了?” 但动态渲染又会牺牲首屏性能,Lighthouse 分数掉得厉害,这不就本末倒置了?

找到瓶颈了!

我先用 next build --profile 跑了一次,配合 Chrome DevTools 的 Performance 面板看构建过程。结果一目了然:90% 的时间花在重复生成那些根本没变的页面上。比如首页、分类页、旧文章,这些内容几个月都不动,但每次构建都重跑一遍 getStaticProps 和 React 渲染。

再翻 Next.js 官方文档,发现他们其实早就在 v9.5 引入了 Incremental Static Regeneration(ISR),但我们的代码还是老式写法——所有页面都用 getStaticPaths + fallback: false,压根没启用增量能力。

折腾了半天发现,问题不在工具,而在我们没用对。

核心改动:从全量重建到增量更新

ISR 的核心思想很简单:只重新生成变化的页面,其他页面保留旧的静态文件。Next.js 通过 revalidate 参数控制缓存时间,但对我们这种“内容由 CMS 触发更新”的场景,更合适的是用 on-demand revalidation(按需重验证)。

具体怎么改?分两步:

  1. 把页面的 getStaticPaths 改成 fallback: 'blocking'true(我们选了 blocking,避免闪加载)
  2. 在 API 路由里接收 CMS 的 webhook,调用 res.revalidate 触发单个页面重建

来看关键代码对比。

优化前(全量 SSG):

// pages/posts/[id].js
export async function getStaticPaths() {
  const posts = await fetch('https://jztheme.com/api/posts').then(res => res.json());
  return {
    paths: posts.map(post => ({ params: { id: post.id } })),
    fallback: false, // 关键问题:一旦构建完成,无法新增页面
  };
}

export async function getStaticProps({ params }) {
  const post = await fetch(https://jztheme.com/api/posts/${params.id}).then(res => res.json());
  return { props: { post } };
}

这个写法的问题是:如果 CMS 新增了一篇 ID 为 1001 的文章,而构建时没包含它,用户访问就会 404。而且每次构建都拉全量数据,慢得离谱。

优化后(启用 ISR + 按需重验证):

// pages/posts/[id].js
export async function getStaticPaths() {
  // 只预渲染热门或近期文章,比如最近100篇
  const recentPosts = await fetch('https://jztheme.com/api/posts?limit=100').then(res => res.json());
  return {
    paths: recentPosts.map(post => ({ params: { id: post.id } })),
    fallback: 'blocking', // 新页面首次访问时服务端生成,后续走缓存
  };
}

export async function getStaticProps({ params }) {
  const post = await fetch(https://jztheme.com/api/posts/${params.id}).then(res => res.json());
  return {
    props: { post },
    revalidate: 3600, // 1小时后自动后台更新,防失效
  };
}

然后加一个 API 路由,用于接收 CMS 的更新通知:

// pages/api/revalidate.js
export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ message: 'Method not allowed' });
  }

  const { secret, id } = req.body;

  // 验证密钥(实际项目中应从环境变量读取)
  if (secret !== process.env.REVALIDATE_SECRET) {
    return res.status(401).json({ message: 'Invalid token' });
  }

  try {
    // 触发指定页面的重新生成
    await res.revalidate(/posts/${id});
    return res.json({ revalidated: true });
  } catch (err) {
    return res.status(500).json({ message: 'Revalidation failed' });
  }
}

CMS 那边配个 webhook,每当文章发布或更新,就 POST 到 /api/revalidate,带上文章 ID 和密钥。这样,只有被修改的页面会被重建,其他几千个页面原封不动,直接从 CDN 返回。

这里注意我踩过好几次坑:一开始忘了设 fallback: 'blocking',导致新文章首次访问返回 404;后来又因为没加 revalidate,万一 webhook 失败,页面就永远卡在旧版本。现在这个组合拳,既保证了即时性,又有兜底策略。

性能数据对比

改完之后,效果立竿见影:

  • 全量构建时间:从平均 15 分钟降到 2 分钟(只预渲染 100 个页面)
  • 单页面更新延迟:从 15 分钟(等全站部署)降到 3 秒内(CMS 发布后立刻可访问)
  • CDN 命中率:从 70% 提升到 98%(因为大部分页面不再变动,长期缓存)
  • 首屏加载时间:保持在 800ms 左右(和之前一样快,因为还是纯静态)

最爽的是开发体验:现在改完文章,点发布,刷一下页面,新内容就出来了。再也不用盯着 CI 日志干等。

当然,也不是完美无缺。比如如果 CMS webhook 挂了,页面就不会自动更新(但我们加了定时任务每小时全量 revalidate 一次作为保底)。另外,对于极高频更新的页面(比如实时数据看板),ISR 还是不太合适,那种场景可能得用 SWR + CSR 混合方案。但对我们这种以内容为主的站点,ISR 简直是量身定制。

最后提醒几个细节

如果你也要上 ISR,这几个点一定注意:

  • 别在 getStaticProps 里做耗时操作,比如复杂计算或大文件处理,ISR 重建时会阻塞请求
  • CDN 缓存头要配对,确保 stale-while-revalidate 能生效(Vercel 默认处理好了,自建的话得自己配)
  • 测试时用 next start 跑生产模式,开发模式下 ISR 不生效

亲测有效,这套方案已经在线上跑了三周,零故障。构建时间省了 85%,团队幸福感飙升。

以上是我个人对增量静态生成的优化实战总结,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多,比如结合边缘函数做个性化缓存,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论