CDN缓存策略与资源加载优化的实战经验分享

ლ红会 优化 阅读 1,205
赞 30 收藏
二维码
手机扫码查看
反馈

谁更灵活?谁更省事?

最近上线一个静态博客站点,访问量不大但用户分布挺散——东南亚、南美、东欧都有人看。本来图省事直接扔在 GitHub Pages 上,结果某天被朋友吐槽“点开要等三秒”,我打开 DevTools 一看:字体加载慢、图片没压缩、JS 脚本全走美国节点……好家伙,这不是拿用户当测速仪嘛。

CDN缓存策略与资源加载优化的实战经验分享

于是我又把 CDN 拿出来重新过了一遍。不是那种“CDN 很好,大家都该用”的泛泛而谈,而是真刀真枪地试了三种主流方案:手动配置资源路径 + 自建 CDN 域名、Vite 插件自动注入、还有服务端渲染(SSR)场景下用 Node.js 中间层动态 rewrite。最后还顺手压测了下真实加载耗时——结果和我预想的不太一样。

方案一:手动改 public 目录 + 静态资源域名(最土但最稳)

这是我最早用的法子,也是我现在给小项目兜底的首选。不依赖构建工具、不卡版本、不搞魔法配置,就是纯手改路径。

比如我把所有静态资源都托管在 https://cdn.jztheme.com(注意:仅示例用,不是官网,也不是我们公司运营的),然后在 public/index.html 里写死:

<link rel="stylesheet" href="https://cdn.jztheme.com/css/main.abc123.css">
<script src="https://cdn.jztheme.com/js/app.def456.js"></script>

同时配个 vite.config.ts 把打包后的资源路径也对齐:

export default defineConfig({
  build: {
    assetsDir: 'js',
    rollupOptions: {
      output: {
        assetFileNames: (assetInfo) => {
          if (assetInfo.name.endsWith('.css')) return 'css/[name].[hash].css'
          return 'js/[name].[hash].[ext]'
        }
      }
    }
  },
  base: 'https://cdn.jztheme.com/'
})

优点是啥?稳定、可预测、调试方便。哪天 CDN 出问题,我直接切回本地路径,5 分钟搞定。缺点呢?每次发版得手动更新 HTML 里的哈希值(或者干脆让 CI 自动替换),还有个坑是:如果你用了 <link rel="preload">,它不会自动跟着 base 走,得自己补全完整 URL —— 我就在这儿踩过两次坑,第一次 preload 加载失败,控制台报 CORS,折腾半天才发现是相对路径没转全。

方案二:Vite 插件自动注入(最爽但有隐藏成本)

我一度以为这就是终极解法。装个 vite-plugin-cdn,几行配置完事:

import { defineConfig } from 'vite'
import cdn from 'vite-plugin-cdn'

export default defineConfig({
  plugins: [
    cdn({
      modules: [
        { name: 'vue', var: 'Vue', path: 'https://cdn.jztheme.com/vue/3.4.21/vue.esm-browser.js' },
        { name: 'lodash', var: 'lodash', path: 'https://cdn.jztheme.com/lodash/4.17.21/lodash.min.js' }
      ]
    })
  ]
})

看起来很美,但实际用起来我发现几个硬伤:

  • 只处理 import 的第三方包,不处理你 public 下的图片、字体、SVG;
  • 如果项目里混用了 ESM 和 UMD 包(比如某些老 UI 库),插件可能识别错入口,打包后 runtime 报 undefined is not a function
  • CDN 地址写死在插件配置里,没法按环境切换(dev/staging/prod),最后我还是得写个 wrapper 脚本去读 .env。

所以这个方案我只在纯 Vue 组件库 demo 项目里用。真到上线项目,我宁可多敲几行代码,也不愿半夜被 Sentry 弹窗叫醒查“为什么 lodash 没定义”。

方案三:Node.js 中间层动态 rewrite(最重但最可控)

这是我在一个 Next.js 项目里试的。前端构建产物扔 OSS,Nginx 或 Cloudflare Workers 不直接代理静态文件,而是交由一个轻量 Express 服务做中间转发:

// server.js
app.get('/static/:file(*)', (req, res) => {
  const cdnHost = process.env.NODE_ENV === 'production'
    ? 'https://cdn.jztheme.com'
    : 'http://localhost:5173'
  res.redirect(302, ${cdnHost}/static/${req.params.file})
})

这样做的好处是:CDN 域名可以运行时决定,还能加鉴权头、灰度开关、甚至根据 User-Agent 动态返回 WebP/JPEG。但代价也很明显:多一层服务、多一个监控点、部署变复杂。上个月我们 Redis 挂了,连带这个 redirect 服务超时,结果所有静态资源 504 —— 这种级联故障,在手动方案里根本不存在。

所以我现在只在需要 A/B 测试、灰度发布或强制 HTTPS 降级(比如某些内网环境)的项目里才上这招。

性能对比:差距比我想象的大

我拿同一份构建产物,在三个方案下跑了 10 次 Lighthouse(移动端模拟,3G 网络):

  • 手动改路径:FCP 平均 1.8s,TTFB 42ms(CDN 缓存命中率 99.2%);
  • Vite 插件:FCP 2.1s,TTFB 58ms(部分资源走的是 unpkg 回源,不稳定);
  • Node 中间层:FCP 1.9s,但 TTFB 94ms(多了跳转耗时,且首屏 JS 加载受阻于 302)。

别小看这 0.3 秒。在我测试的东南亚用户设备上,2.1s 和 1.8s 的感知差异,就是“再刷一次”和“直接关掉”的区别。

我的选型逻辑

总结一下我的日常操作流:

  • 静态站点(Vue/Vite/Hugo):一律手动改 base + public/index.html,配合 CI 自动替换哈希;
  • 中后台系统(Ant Design Pro / umi):用 webpack 的 publicPath + CDN 域名变量,不碰插件;
  • 需要灰度/AB 的 SSR 项目(Next/Nuxt):上中间层,但会加熔断,失败直接 fallback 到 OSS 原始地址;
  • 小程序或 PWA?别扯 CDN 了,资源全打到包里,CDN 对离线体验没意义。

没有银弹。我见过团队为图省事强行用 Vite 插件,结果上线后发现图标全裂开(SVG sprite 路径没被插件捕获),回滚花了两小时;也见过用中间层的同学,为了少写一行 redirect,把整个 Nginx 配置重写了三遍……技术方案从来不是越新越好,而是看你能不能在凌晨两点快速定位并修复它。

以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论
Newb.宁蒙
这篇文章逻辑太清晰了,读起来特别顺畅!
点赞 3
2026-02-10 23:25