网站修复记录
Vercel 回源用量告警:把图库筛选改回静态交付
一次 Fast Origin Transfer 告警,暴露了爬虫、组合筛选地址和动态渲染的叠加成本。记录如何保留导航体验,同时让书法与摄影内容恢复静态交付。
目录
Vercel 发来一条通知:免费团队的 Fast Origin Transfer 已用掉 10 GB 额度的 75%。这个网站有摄影、书法图片,也有一个 3D 首页,很容易先怀疑大文件。查完实际请求以后,问题却集中在书法图库:固定的作品内容,因为筛选参数和爬虫抓取,被反复交给函数渲染。
这次调整的目标很具体:尽量让个人站点维持在免费额度内,日常现金成本基本只剩域名。为此,需要先弄清楚哪一段传输在消耗配额,再决定是否升级套餐或换服务器。
先认清告警里的流量
Vercel 的两个指标不能混为一谈:Fast Data Transfer 统计访客与 CDN 之间的传输;Fast Origin Transfer 统计 CDN 与函数、Middleware、缓存等服务之间的传输。同一个请求可能经过多个阶段,函数响应命中 CDN 缓存后,则可以减少后续回源。调查当日,Hobby 对应的包含量分别为 100 GB 和 10 GB。Vercel CDN 用量说明
截图只说明团队的一项额度接近上限。通知上的“8h”不是“8 小时内用了 7.5 GB”,也不能把请求数当作真人访问数。
2026 年 9 月 6 日上午,我进一步读取了项目后台的 Last 30 Days 视图:
| 指标 | 当时用量 |
|---|---|
| Fast Origin Transfer | 约 7.72 GB / 10 GB |
| Fast Data Transfer | 约 6.08 GB / 100 GB |
| Function Invocations | 512,779 次 |
| Active CPU | 约 2 小时 20 分 |
团队回源量的 99.9% 来自这个项目。但这只是过去 30 天的观察窗口,不能直接当成自然月账单,也不能据此预测下个月的费用。
图片的路径也很关键:摄影、书法和博客图片通过 img.huzejun.com 直接交付,相关组件使用 unoptimized,没有让 Vercel 代为处理这些图片。3D GLB 则是带长期缓存的同源静态文件。它们当然影响下载量,但本次证据没有把它们指向回源告警的主要来源。
95 条记录,为什么会反复调用函数
最近 12 小时的函数视图提供了更明确的线索:
| 路由 | 函数调用 | 页面缓存命中率 |
|---|---|---|
| 书法图库 | 约 6.9K | 0% |
| 书法详情 | 约 4.7K | 0% |
| 摄影详情 | 31 | 0% |
同一时段的机器人分组中,Amazonbot 约有 10K 次请求,缓存命中率只有 0.1%。日志样本里,一次图库请求同时带着类型、书体、系列、标签和年份等六个条件,随后执行了语言 Middleware 和页面函数。
这支持一个具体判断:机器人正在抓取大量筛选组合,动态页面又让这些请求持续回源。它不代表我已经把整月每个字节都归因到某个机器人。
图库只有 95 条书法记录,但每条记录和每种筛选都可以继续产生链接。两个地址可能展示相同或高度重叠的内容:
/zh/calligraphy/gallery?kind=practice
/zh/calligraphy/gallery?kind=practice&year=2020作品详情还会携带这些参数,让“上一张”“下一张”和“返回图库”保留当前范围。这个导航设计有用,问题出在实现位置:服务端页面读取了 searchParams。
在本项目使用的 Next.js 16.3 中,读取页面的服务端 searchParams 会引入请求期渲染。即使详情页写了 generateStaticParams,也不能据此认定最终响应已经静态化。Next.js 页面参数文档
优化前,抽查响应带有 private, no-cache, no-store,而生产构建清单中缺少这些页面的完整预生成结果。这比“代码看起来是静态的”更能说明实际交付方式。
把内容与浏览上下文分开
作品正文、图片地址和元数据只会在内容发布时变化;筛选条件和阅读位置则随着访客操作变化。我把这两部分拆开处理。
服务端在构建时生成默认图库和每件作品的完整内容,不再等待筛选参数。浏览器读取 URL,在已有数据上计算筛选结果、相邻作品和返回链接。URL 仍然可以分享,前进后退也继续恢复筛选状态。
这里有一个容易遗漏的边界:useSearchParams 放在很小的 Suspense 子组件里,负责通知外层更新查询状态;正文留在这个边界之外。这样静态 HTML 保留可阅读内容,不会让整个图库都等到 JavaScript 执行后才出现。useSearchParams 与静态渲染
直接打开带筛选参数的地址时,先收到默认静态内容,再由浏览器应用筛选。这是这次方案明确接受的取舍;正文仍在初始 HTML 中,但筛选状态的恢复依赖 JavaScript。
筛选操作也改用原生历史记录:离散条件使用 pushState,搜索输入使用 replaceState,避免每输入一个字就增加一条后退记录。Next.js 支持不整页刷新地更新历史,并把这些操作同步给 useSearchParams;本项目是否产生额外数据请求,则通过实际请求日志验证。原生 History API
快速连续操作时,新条件从点击当刻的 URL 合并,不能只依赖上一次渲染时捕获的状态,否则“先选类型,再选媒介”可能丢掉前一个条件。
为了让浏览器能够计算详情导航,我还把书法的纯数据逻辑与生成内容的读取入口分离。详情只额外携带导航所需字段,95 条记录的导航索引从完整数据的 180,672 B 缩至 78,160 B。这个 57% 是索引数据的缩减,不能当成网站总流量或 Vercel 费用的下降比例。
减少不必要的入口和抓取
内容静态化之外,还有几处配套调整:
/zh/...和/en/...已经明确语言,直接进入预生成路由;只有根入口和不带语言的地址需要语言协商与跳转。- 公开内容参数在构建时闭合,未知作品或语言返回 404。旧书法别名使用配置中的永久重定向,并保留查询参数。
- 画廊和作品补充 canonical;带筛选上下文的链接关闭自动预取,并增加
nofollow。 robots.txt排除归档查询组合,并向此次识别到的 Amazonbot、SemrushBot 声明禁止抓取。
robots 和 nofollow 都不能承担硬性流量限制。机器人可能缓存规则,也可能不遵守;因此即使旧地址继续被访问,页面仍应依靠静态交付减少函数执行。
验证的是交付方式,降幅还要看线上
我为生产构建增加了 delivery:verify。它读取实际构建清单和 sitemap,检查公开路由是否有完整预生成结果、是否还允许未知参数触发运行时生成、缓存策略是否变回私有。首页保留有下限的 ISR,用来更新最近收听内容。
优化完成、加入本文之前,检查覆盖了 360 个公开中英文路径和 12 个闭合路由模板,完整 422 项测试通过。书法完整数据与精简导航索引还进行了 1,425 组查询对照,核对位置、相邻作品及相关推荐是否一致。
本地生产模式的 HTTP 检查中,带筛选参数的书法图库和标准地址返回同一份 527,797 B 的 HTML,均带有:
x-nextjs-cache: HIT
Cache-Control: s-maxage=31536000这是预生成内容的缓存结果,没有给任意动态响应强行套一年的缓存。它也不是 Vercel 生产边缘命中率的实测值。
浏览器验证了筛选、刷新、前进后退、作品键盘导航、中英文和明暗界面。本地请求日志进一步确认:书法连续选择两个条件、摄影搜索叠加年份,都没有新增页面或 RSC 请求。
这些结果证明不必要的动态工作已被移走,尚不能证明账单下降了某个百分比。部署后需要比较每天新增的回源量、函数调用与 CPU 时间;原有的 30 天累计值不会因一次发布立即清零。
是否还需要 Pro 或自己的服务器
截至调查当日,Vercel Pro 的基础费用为每月 20 美元,包含一个部署席位和每月 20 美元的用量抵扣;超过抵扣仍会产生按量费用。Vercel Pro 说明
对这个项目,先保留 Hobby、减少重复渲染更符合当前目标。自购服务器可能降低长期付费托管的基础月租,但同时需要承担部署、补丁、备份和故障处理;这条额度通知本身不足以支持搬迁决定。
R2 还要单独核对。Standard 免费层包含每月 10 GB-month 存储、100 万次 Class A 和 1,000 万次 Class B 操作,互联网出站免费。没有收到告警不等于已经确认账单为零。R2 价格说明
目前的目标仍是日常只承担域名费,但首页 ISR、音乐封面代理、缓存未命中和异常流量都可能继续消耗额度。比承诺永久免费更有用的做法,是让固定内容稳定地静态交付,并把“页面意外变动态”变成构建时就能发现的问题。

