Astro 内容站一旦过千页,改一个错别字都可能触发好几分钟的全量重建,外加一次把所有 URL 都刷一遍的 CDN 失效。Astro 没有哪个”增量构建”开关能一键搞定,但截至 2026 年 6 月,Astro 5 的 Content Layer 缓存 + 给少数每天都变的页面上按需渲染 + 按文件刷新 CDN 这套组合,能解决大约九成的问题。下面这套工作流,就是我在一个每语言约 3000 页的双语站上实际跑的。
一句话结论
- 500 页以下: 直接重建。不加任何额外机制反而最快。
- 500-5000 页: 升级到 Astro 5,让 Content Layer 在两次构建间缓存解析结果(Markdown 最高快 5 倍、MDX 快 2 倍、内存省 25-50%),再用内容 hash 卡 CI,并交给托管平台按文件刷新。
- 超过 5000 页: 把每天都会变的页面(首页、最新列表、RSS、sitemap)搬到按需渲染,这样改一篇文章只会让那一个静态页加几个服务端路由失效,而不是整站。
- 唯一真正的坑: Astro 5 已经移除了
output: 'hybrid'模式。改用默认的output: 'static',再用export const prerender = false;给单个路由开按需渲染。
为什么 Astro 会重建整站
Astro 的构建天生是确定性的:在 output: 'static' 模式下,它把每个页面都从源头重新生成,保证输出能从源码完整复现。这对正确性很好,但对热编辑很糟——改一个词,3000 个页面照样全走一遍。它没有那种”只有这两个页面依赖了这个文件”的 diff 步骤。所以解决办法不是某个神奇 flag,而是构建期缓存、对真正不想重建的少数页面用按需渲染、再加 CDN 层局部失效这三件事的组合。
哪些信号说明你不该再裸重建
npm run build超过 90 秒,而且每加一篇文章还在涨。- 小改你都攒着”等周末统一发”,而不是立刻上线。
- 每次部署 CDN 全量 purge,把免费额度吃光。
- CI 时长大头是 Astro 页面生成,不是测试或 lint。
- 你已经开始在部署前脚本里写跳过构建的 hack。
如果以上一条都不沾边,看到这里就够了。用不上的复杂度本身就是一种债。
第一步:把 Content Layer 缓存跑起来(Astro 5+)
2026 年最大的免费收益,就是升级到 Astro 5、让内容集合走 Content Layer API。按 Astro 官方给的数字,内容集合在内容密集型站点上 Markdown 最高快 5 倍、MDX 最高快 2 倍,内存占用降 25-50%。Content Layer 会维护一个在两次构建间持久化的本地数据存储(放在 .astro/ 下),它的 loader 用 digest 校验,没变过的条目就不会重新处理。
用 glob loader 定义集合,让文件改动被这个存储追踪:
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const articles = defineCollection({
loader: glob({ pattern: '**/*.mdx', base: './src/content/articles' }),
schema: z.object({
title: z.string(),
description: z.string(),
publishedAt: z.date(),
draft: z.boolean().default(false),
}),
});
export const collections = { articles };
然后在 CI 里把 .astro/ 跨次运行缓存下来。正是这个缓存,才让解析提速在 CI 里也体现出来,而不只是在你本机:
# .github/workflows/deploy.yml(节选)
- uses: actions/cache@v4
with:
path: |
node_modules/.astro
.astro
key: astro-store-${{ hashFiles('src/content/**/*.mdx') }}
restore-keys: astro-store-
这缓存的是解析阶段,缓存不了静态 HTML 生成阶段本身,所以它砍的是构建的前半段,不是后半段。这个区别在下一步很关键。
第二步:用内容 hash 卡 CI
最便宜的跳过,就是检测到没真改东西就直接短路整个构建。把每个内容文件(连同 frontmatter,因为只改 frontmatter 也会改变渲染出的 HTML)加上 layout 文件一起 hash,manifest 一致就提前退出:
// scripts/build-if-changed.mjs
import { createHash } from 'node:crypto';
import { readdirSync, readFileSync, existsSync, writeFileSync } from 'node:fs';
const inputs = [
...readdirSync('src/content', { recursive: true }).map(f => `src/content/${f}`),
...readdirSync('src/layouts', { recursive: true }).map(f => `src/layouts/${f}`),
].filter(f => /\.(mdx|md|astro|ts)$/.test(f));
const hash = createHash('sha256');
for (const f of inputs.sort()) hash.update(readFileSync(f));
const digest = hash.digest('hex');
const last = existsSync('.build-hash') ? readFileSync('.build-hash', 'utf8') : '';
if (digest === last) {
console.log('内容和 layout 都没变,跳过构建。');
process.exit(0);
}
writeFileSync('.build-hash', digest);
process.exit(1); // 非零退出码让 CI 继续走构建步骤
单条错别字的部署它救不了,但能挡掉相当一大批”只改了文档、依赖升级、改 workflow”的提交。.build-hash 用和 .astro/ 一样的方式持久化。
第三步:把每天都变的页面搬到按需渲染
对一个月才变一次的文章页,静态生成是对的默认。对首页、“最新”列表、RSS、sitemap 这些每次发文都会变的页面,它就是错的默认。把这些做成按需渲染,改一篇文章就只会让那一个静态文章页加这几个服务端路由失效,而不是为了刷新一个列表去全量重建。
Astro 5 移除了旧的 output: 'hybrid' 模式。保留默认的 output: 'static',按文件给动态路由单独开按需:
// astro.config.mjs
import { defineConfig } from 'astro';
import vercel from '@astrojs/vercel';
export default defineConfig({
output: 'static', // 默认值;不显式 opt-out 的页面都会预渲染
adapter: vercel(),
prefetch: { defaultStrategy: 'viewport' },
});
---
// src/pages/index.astro —— 这个路由按需渲染
export const prerender = false;
const latest = (await getCollection('articles'))
.sort((a, b) => +b.data.publishedAt - +a.data.publishedAt)
.slice(0, 20);
---
这样你那 3000 个文章页一直是静态、永久可缓存,而少数几个索引页一发文就立刻反映新内容,不用重建。
第四步:只刷变化的那几条路径
构建完成后,把生成的 dist/ 和上次部署做 diff,只 purge 变化的 HTML 文件,而不是整个 zone:
# diff 两次部署的 dist/,只 purge 变化的路径
git -C dist diff --name-only HEAD~1 HEAD -- '**/*.html' \
| sed 's#^#https://yourdomain.com/#' \
| jq -R -s -c 'split("\n")[:-1] | {files: .}' \
| curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE/purge_cache" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
--data @-
大多数托管平台已经按内容 hash 做按文件失效了,所以在它们上面你基本用不上这段:
| 平台 | 部署时按文件刷新 | 计划备注(2026 年 6 月) |
|---|---|---|
| Vercel | 自动(不可变、按 hash 寻址) | Hobby 免费、不可商用;Pro $20/席 |
| Cloudflare Pages | 自动 | 免费额度很大方 |
| Netlify | 自动(原子部署) | 有免费档;可经 API 按需 purge |
| Firebase Hosting | 不自动按文件刷新 | Spark 免费、可商用;purge 得自己写 |
| 裸 S3 + CloudFront | 否 | 失效得自己写脚本 |
上面这段脚本,只在 Firebase Hosting、S3/CloudFront,或任何你直接掌控缓存层的场景下才值得写。
容易踩的坑
- 200 页的小站也上按需渲染。冷启动和配置开销不划算,老老实实重建就行。
- 照着旧教程用
output: 'hybrid'。这个模式在 Astro 5 里已经不存在,构建会直接报错。 - 习惯性每次部署全站 purge——而你的平台其实早就按文件局部失效了。
- 自己造增量构建工具,重新实现 Astro 内部逻辑,下个 minor 版本就崩。
- 只 hash 文章正文,漏掉 frontmatter 或 layout。这两样都会改变渲染出的 HTML。
- 拿 dev 模式的 HMR 当生产增量替代品。dev 模式是按需出页的,根本不会走完整的静态生成路径。
FAQ
- Astro 有没有一键的官方增量构建:截至 2026 年 6 月没有。Astro 5 的 Content Layer 缓存了数据加载和解析,但静态 HTML 生成还是会走遍每个页面。生产环境的答案,仍然是上面这套缓存 + 按需路由 + 局部刷新的组合。
- Content Layer 到底能给内容密集型构建提速多少:Astro 给的数字是 Markdown 集合构建最高快 5 倍、MDX 最高快 2 倍、内存降 25-50%。提速在解析和数据加载这一环,HTML 渲染阶段不变,所以预期是构建前半段缩短,而不是整体砍半。
output: 'hybrid'被什么取代了:Astro 5 把模式统一了。用output: 'static'(默认,全部预渲染),在想服务端渲染的路由上加export const prerender = false;。只有当整站大部分是动态时,才用output: 'server'。- CI 里缓存
.astro/和node_modules够不够:它缓存的是依赖安装和解析,大站能省几秒到一分钟,但省不到主导 3000 页构建的页面生成那一步。配合内容 hash 卡 CI,才是 CI 上最大的收益。 - Firebase Hosting 能跑按需渲染的页面吗:能,走 Cloud Functions 或 Cloud Run,但对低流量的索引页,冷启动延迟常常把好处吃掉。按需渲染配合 Vercel、Netlify 或 Cloudflare 最顺。
- 改一次内容必须刷新 sitemap 吗:只有当准确的
lastmod对你的 SEO 重要时才需要。把 sitemap 做成按需路由,不用重建也永远是最新的。