这个博客的第一版是 2020 年搭的,Hugo + LoveIt 主题,写完直接 hugo 生成一堆 HTML 推到 GitHub 仓库,再接 Cloudflare Pages。能用,但一直有个心结:打开很慢。
这次索性推倒重来。结论先行:首页从 7.06 秒 → 1.1 秒,外部请求从 8 个 → 0 个,页面体积从 200 多 KB → 28 KB。下面是完整过程,包括踩的坑。

一、先体检,别急着换工具
动手前我做了一件最该做的事:量化。
用 curl 把首页所有资源逐个打了一遍,结果很刺眼:
| 资源 | 来源 | 耗时 | 大小 |
|---|---|---|---|
| fontawesome-free CSS | jsdelivr | 4.40 s | 100 KB |
| lunr.min.js(搜索) | jsdelivr | 7.45 s | 29 KB |
| animate.css / clipboard / lazysizes… | jsdelivr | 同类量级 | — |
| 自己的 HTML | 本站 | 0.65–2.0 s | 16 KB |
| 首页总加载 | 7.06 s |
也就是说,服务器那 1 到 2 秒是正常的,剩下的 5 秒全在等别人的 CDN。
根因不难理解:jsdelivr 早年在中国大陆有加速节点,后来撤了,现在国内访问要绕到境外;而我的 Cloudflare 边缘又恰好落在阿姆斯特丹,一次 TCP + TLS 握手就是 250 毫秒起步,8 个资源叠起来就是好几秒。
这里有个方法论:慢的时候先分清是「我的问题」还是「别人的问题」。换主机、换框架是最容易想到的动作,但往往不是解法。
二、选型:三件事,不换的两件
生成器:不换
Hugo 已经是对的选择:单二进制、零运行时依赖、几百篇文章构建不到一秒、Cloudflare Pages 原生支持。换成 Astro 或 Hexo 只会增加构建复杂度,对这个博客没有任何收益。
主题:必须换
问题不在 Hugo,在主题。我抓了 11 个主流 Hugo 主题的 demo 站,只提取真实的资源请求(<script src>、<link rel=stylesheet>、<img src> 的域名),不猜、不凭印象:
| 主题 | Star | 最近更新 | 外部资源引用 |
|---|---|---|---|
| PaperMod | 14k | 活跃 | 0 |
| hugo-book | 4.1k | 活跃 | 0 |
| hugo-coder | 3.1k | 2026-06 | 0(可选功能才拉 CDN) |
| Blowfish | 2.9k | 活跃 | Google Tag Manager + cdnjs |
| Stack | 6.5k | 2026-05 | Google Fonts |
| LoveIt(原来用的) | 3.9k | 活跃 | jsdelivr |
| PaperModX | 392 | 2025-03 停更 | 0 |
PaperMod 是唯一同时满足「开箱零外链 + 持续维护 + 信息密度高」的。而且它有个隐藏优势:纯 CSS,不含任何 SCSS,所以不需要 Hugo extended 版本——在 CI 里少一个不确定因素。
它的字体直接用系统字体栈(-apple-system, "Microsoft YaHei"…),图标是内联 SVG,搜索用本地打包的 Fuse.js,全程不碰第三方域名。
部署:继续用 Cloudflare Pages,但换成「源码仓库」
原来的模式是:本地构建 → 把 public/ 推上 GitHub → Pages 直接发布产物。这叫产物仓库,问题是你没法在网页上改文章、也没法让 CI 帮你构建。
这次改成:仓库只存源码,Pages 直连私有仓库构建。
写作 → git push(私有仓库)
↓ Cloudflare Pages 自动构建(hugo --gc --minify,约 20 秒)
↓
blog.xbta.cc(CF 边缘)
顺带说个容易被忽略的事实:Pages 的静态请求和带宽是无限免费的,只有 Pages Functions 才计入 Workers 配额。免费版的限制只有 500 次构建/月、2 万个文件、单文件 25 MB——对个人博客完全够用。
三、改造:不碰主题源码
我一上来给自己定了条硬规矩:themes/PaperMod/ 一个字节都不许改。
理由是可持续性。改主题源码的后果是:主题一升级就冲突,改了三处忘了一处,半年后自己都看不懂改了什么。正确姿势是把所有定制落在两个地方:
| 位置 | 干什么 |
|---|---|
hugo.yaml | 站点与主题参数(默认浅色、目录、菜单、分页、搜索配置) |
assets/css/extended/custom.css | 字号、行宽、表格、代码块、列表密度 |
assets/css/extended/ 是 PaperMod 官方指定的定制入口,主题会主动加载它。这样以后升级主题,直接整个目录覆盖,零冲突。
具体定制按我自己的阅读习惯来:正文 760px、17px 字号、1.75 行高、浅色主题默认、列表不要卡片(一条细线分隔就够)、表格横向滚动不撑破版心。
四、图片:14 MB → 1.5 MB
36 张历史配图原来挂在 jsdelivr 的 GitHub 直链上,同样慢。这次全部迁到自己的 R2 图床:
- 格式:Pillow 转 WebP,质量 82,长边限制 1600px
- 结果:14 MB → 1.48 MB,省了 89%(单张典型值 225 KB → 28 KB)
- 缓存:对象上直接写
Cache-Control: public, max-age=31536000, immutable - 访问:
img.xbta.cc/blog/2020/xxx.webp
这里有个反直觉的发现:R2 的自定义域名不走 Cloudflare 的缓存层。我一开始给它配了一条「边缘缓存一年」的 Cache Rule,测出来 cf-cache-status 永远是 DYNAMIC,规则完全不生效。真正起作用的是对象自带的 Cache-Control——浏览器第二次访问直接命中本地缓存,零请求。
也就是说:R2 靠的是「浏览器缓存」而不是「边缘缓存」。知道这一点,就不用再浪费时间调规则了。
五、结果
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 首页完全加载 | 7.06 s | 约 1.1 s |
| 外部资源请求 | 8 个 | 0 |
| 首页 HTML | 16 KB + 8 个跨域 | 8.8 KB(gzip 3.4 KB) |
| CSS | 71 KB + 外链 100 KB | 19.7 KB(gzip 5.1 KB) |
| 图片总量 | 14 MB(境外直链) | 1.48 MB(自有图床) |
| LCP / CLS | — | 1.1–1.4 s / 0.000 |
现在整个首屏就是一个 HTML + 一个 CSS,加起来 28 KB,剩下的全是可缓存的图片。
六、踩过的四个坑
1. 图片下载的 403 不是限流,是 UA
批量迁移图片时,脚本一直返回 403。我先怀疑是代理限流,换了两轮实现才发现:python urllib 的默认 User-Agent 被源站拒了,加上浏览器 UA 立刻正常。
教训:批量跑之前先用一张图把链路打通(UA、长度校验、能不能被解析),别写完脚本再来试错。
2. 旧主题的短代码,新主题没有
文章里用了 LoveIt 的 {{< admonition >}} 提示块,换主题后构建直接失败:template for shortcode "admonition" not found。迁内容前应该先 grep 一遍所有短代码。
3. 改卡片样式漏了一个 border
PaperMod 的文章列表项默认是卡片(背景 + 圆角 + 边框 + 悬浮上移)。我去掉了背景和圆角,截图一看还是卡片——因为 border: 1px solid 还在。覆盖第三方样式必须对照它的源 CSS 逐条覆盖,只改显眼的那几个属性一定留尾巴。
4. 窄屏代码块溢出,首屏截图看不出来
手机宽度下代码块被右侧裁切,但首页截图完全正常——问题在页面下半部分。后来改成整页长截图 + 直接读 scrollWidth 和 clientWidth 才定位到。视觉验收不能只看首屏。
七、一点想法:天花板在哪
这套方案把「我能控制的部分」做到底了,但还有一段绕不过去:Cloudflare 免费版从中国大陆访问,TTFB 在 0.6–2 秒之间波动,因为边缘节点稳定落在我控制不了的地方(实测是阿姆斯特丹)。
这不是代码能优化掉的,是地理位置。想根治只有一条路:域名备案 + 境内 CDN,那是另一个成本和流程问题。
所以我的策略是承认它、然后绕过它:既然单次往返省不掉,那就让往返次数降到最少。1 个 HTML + 1 个 CSS,第一个字节到了页面就基本成型,剩下的图片慢慢来也不影响阅读。
体感上的「快」,很多时候不是 RTT 变小了,而是等待的东西变少了。
最后
这次重建真正的收获不是换了什么主题,而是那句最朴素的话:先量,再改。
如果不做那次体检,我大概会以为是自己服务器不行,去折腾主机配置、上什么加速服务——而那些全都解决不了问题,因为问题从头到尾都在别人的 CDN 上。