4.1 CDN 接入:缓存策略与刷新
站已经托管上云了。第三篇我们走通了 COS、域名和 SSL,页面能通过 HTTPS 正常打开。但上线第一天合伙人就发来截图:首页首屏要三秒多,轮播图转半天。本地调试时明明是秒开的,这差距从哪来的?
场景:源站在广州,用户在全国
我们的站部署在腾讯云网站托管上,源站资源在华南。本地访问秒开,是因为我们本来就在同城;但跨地域访问时,每个 JS、CSS、图片都要回源拉取,一来一回延迟叠加,首屏自然就慢。创业页是要发给投资人看的,他们在各个城市,加载速度就是第一印象,慢三秒可能页面就被直接关掉了。
解决方案是 CDN:把静态资源缓存到全国各地的边缘节点,用户就近取内容,不用每次都回源。CDN 的官方文档在 cloud.tencent.com/document/product/228,接入流程和接口说明都很全。网站托管服务本身已经内置了 CDN 加速能力,但在自定义域名、缓存策略、刷新时机这些环节,还有不少可以调优的空间。这一篇记下我们接入和调优 CDN 的全过程,核心就两件事:缓存策略怎么配、内容更新后怎么刷。
想理解为什么这两件事最重要,先看一个心智模型。
方案:缓存是钥匙,刷新是锁
CDN 的缓存是整个加速方案的钥匙。节点上没有缓存时,用户请求会回源一次,拿到的资源按缓存规则存在节点上;之后其他用户再访问同一资源,直接从节点返回,不再回源。所以缓存策略决定两个指标——命中率和新鲜度。缓存时间越久命中率越高,但内容更新后旧缓存残留的时间也越久。
我们的资源分三类,策略各不相同:
| 资源类型 | 路径特征 | 建议缓存时长 | 更新方式 |
|---|---|---|---|
| 带哈希的打包产物 | /assets/xxxx.js | 365 天 | 文件名带哈希,内容变名字就变 |
| 首页入口 HTML | /index.html | 不缓存或 60 秒 | 每次部署后主动刷新 |
| 图片与字体 | /img、/fonts | 30 天 | 覆盖上传后刷新目录 |
关键点在打包产物:一定要用内容哈希命名。Vite 默认就是这么做的,build 出来的 JS 文件名带八位哈希,内容一变名字就变。这样 JS 和 CSS 可以放心缓存一年,而 HTML 里引用的是新文件名,用户刷新首页时自然就拿到新资源。这个约定在第二篇构建脚本里已经打好基础,到这里正好接上,这也是我们把缓存策略和构建脚本放在一起规划的原因。
配置入口在 CDN 控制台的域名管理里,找到缓存配置,按上表一条条加规则。注意规则有优先级,先匹配先生效,要把 HTML 的规则放在最前面,否则后面的通配规则可能先截获请求。保存后规则一般一两分钟内生效,不放心的话可以用响应头里的缓存标记验证:请求一个文件,看返回头里是节点缓存命中还是回源拉取。调规则当天我们反复用这个办法确认,比凭感觉猜要靠谱得多。
配置:把刷新写进部署脚本
缓存配好只是第一步。更实际的问题是:每次部署后,旧 HTML 还躺在节点上,新内容如何立刻生效?答案是刷新。刷新的意思是主动把节点上的缓存标记为失效,下一次请求会回源拉最新内容。
控制台可以手动刷,但每次发包都去点一下太容易忘。我们的做法是把刷新写进部署脚本,部署完自动调用 CDN 接口,这一步在后面的 CI/CD 篇会串进完整流水线。核心代码如下:
// scripts/refresh-cdn.ts:构建部署完成后刷新 CDN 缓存
import CdnClient from "tencentcloud-sdk-nodejs-cdn";
const client = new CdnClient({
credential: {
secretId: process.env.TC_SECRET_ID!,
secretKey: process.env.TC_SECRET_KEY!,
},
region: "ap-guangzhou",
profile: { httpProfile: { endpoint: "cdn.tencentcloudapi.com" } },
});
async function refreshCdn() {
const urls = [
"https://example.com/",
"https://example.com/index.html",
"https://example.com/about",
];
const res = await client.FlushCdnUrl({
Urls: urls,
FlushType: "flush", // flush 清除并立即回源,delete 只删缓存不回源
});
console.log("刷新任务已提交,TaskId:", res.TaskId);
}
refreshCdn().catch((e) => {
console.error("CDN 刷新失败:", e.message);
process.exit(1);
});
这段脚本只做一件事:把几个入口页面加入刷新队列。接口是异步的,提交后会返回一个 TaskId,可以用它查询刷新进度。这在排查问题时很有用——有时候页面还是旧的,先确认任务到底成功了没有,而不是反复怀疑缓存策略没生效。官方接口文档在 CDN 文档中心(cloud.tencent.com/document/product/228)里写得很清楚。
除了刷新,还有一个方向相反的操作:预热(Push)。刷新是清空,预热是提前把资源填充到节点上。新站刚上线或发大版本时,与其等第一个用户触发回源,不如主动把首屏资源推到节点,调用的接口是 PushUrlsCache,参数几乎一样,场景正好互补。
预热适合两种时刻:一是站刚上线,所有节点都没有缓存;二是做了改版,首屏资源换了新文件名,一次预热能让全国用户都秒开。我们固定在每次大版本发布时跑一次预热脚本,小迭代只刷入口页。预热同样占用每日额度,不过配额和刷新分开计算,不用担心互相挤压。
还要注意控制台里的一个开关:是否遵循源站缓存头。如果源站返回了不缓存的响应头,CDN 节点就不会缓存,命中率会低到没法看。我们的做法是在控制台强制使用自定义缓存规则,忽略源站的响应头,这样行为完全可控。
另外说一个回源协议的细节。CDN 节点回源拉资源时,用 HTTPS 还是 HTTP 取决于控制台里的回源协议配置。我们一开始图快用了 HTTP 回源,虽然用户侧是 HTTPS,但节点到源站这段是明文,安全上差点意思。后来改成协议跟随,用户用 HTTPS 访问就 HTTPS 回源,一次配置就没有短板了。证书是第三篇申请的免费 DV 证书,配上之后此处一路加密到底。
踩坑与排查
接入过程踩过几个坑,记下来给你省时间。
第一个是刷新额度。CDN 的 URL 刷新有每日额度上限,默认每天不超过 1000 个 URL、100 个目录。我们的 CI 一开始每次部署都刷全站,包不大但次数多,很快触到限额,接口报错时部署脚本还以为是代码出了问题。解决办法是只刷变化的入口页,带上哈希的资源根本不用刷,名字变了自然就是新请求。后来在监控告警里加了刷新失败告警,才算彻底闭环。
第二个是命中率怎么看。CDN 控制台有命中率曲线,正常应该在九成以上。如果某天命中率突然掉到一半以下,先别怀疑策略,按这个顺序查:先看是不是有大流量回源的新资源(比如刚发布了新版本),再看源站响应头是不是变了,最后才去翻缓存规则。命中率下降往往是回源变多造成的,而不是规则失效。
第三个是刷新目录还是刷新 URL。刷新目录开销大、生效慢,但适合图片这种整体替换的场景;刷新 URL 精准、额度占用多,适合 HTML 这类入口页。我们最后定的规矩:HTML 走 URL 刷新,图片目录整体替换时走目录刷新,两者结合着用。命中率的长期观察可以参考云监控实践,思路是一致的。具体到排查,控制台还有几个现成工具:回源流量明细能看出是哪些 URL 在频繁回源,状态码统计能一眼发现有没有大量 404 被缓存。有一次我们某个旧图标路径删了但外站还在引用,节点上反复缓存 404,就是靠状态码统计抓住的。顺手把这类路径写进刷新清单,清一次干净一次。
小结
CDN 接入本身不复杂,控制台点点就能通。真正难的是把缓存策略、构建产物命名、部署后的刷新串成一条线:构建用内容哈希让静态资源敢长期缓存,部署后自动刷新入口页让新内容立刻可见,再用命中率曲线验证效果。这套组合拳打完,全国访问首屏从三秒多降到 300 毫秒以内,这就是创业页发给投资人之前的底气。还有一个意外收获:因为缓存命中率稳定在九成以上,源站的流量和费用也跟着降了下来,托管账单几乎只剩基础部分。投入产出比一算,CDN 调优可能是整个项目里最划算的一件事。下一篇我们给静态站补上后端,先从留言表单开始。
延伸阅读
- Node.js 网站托管实战目录:全书章节索引
- 轻量服务器建站指南:自建 Nginx 场景下的缓存配置对照
- 腾讯云实践合集:CDN 与其他云产品的组合玩法
