用 AI Agent 把 Hexo 博客从 5 秒白屏优化到 0.5 秒可见

前几天我重新打开自己的博客,第一眼不是慢,而是空。

页面框架已经出来了,左侧栏也在,但文章区域要等大约 5 秒才显示。对一个以文字为主的静态博客来说,这个体验很不合理。于是我让 AI Agent 参与了一次完整排查:测量线上表现、定位源码、修改配置、建立自动化验收、提交代码,再部署到腾讯云。

这篇文章记录实际过程。没有换主题,也没有为了跑分重写整站,主要目标只有一个:先让读者尽快看到文章。

先测量,不凭感觉改

博客使用 Hexo 和 NexT Gemini。线上站点看起来并不大,静态文件总量约 7.7 MB,磁盘空间不是主要矛盾。

真正的问题出现在冷启动:

指标优化前
DOMContentLoaded约 4.17 秒
首次内容绘制 FCP约 4.39 秒
第一篇文章真正可见约 5.12 秒
anime.min.js 加载约 4.06 秒
pace.min.js 加载约 3.79 秒

如果只看 HTML 返回时间,很容易误以为站点没问题。浏览器自动化测量发现,NexT 的 Motion 会先隐藏 .post-block.post-header.post-body,等动画依赖准备好后再显示。

也就是说,文章早已在 HTML 里,却被样式主动藏了起来。外部 CDN 稍微慢一点,读者看到的就是一大片空白。

这个结论比“服务器带宽不够”靠谱得多,因为它能解释为什么页面外壳出现了,正文却迟迟不见。

第一刀:关闭不必要的首屏动画

博客的价值是内容,不是入场动画。我最终关闭了 Motion、Pace 和 PJAX,同时只保留一种图片缩放方案:

1
2
3
4
5
6
7
8
9
10
motion:
enable: false

pace:
enable: false

pjax: false
fancybox: false
mediumzoom: true
lazyload: true

关闭 Motion 后,文章不再默认隐藏。即使后面的 JavaScript 还在加载,正文也能先显示。

Pace 的进度条同样被移除。它并没有让页面更快,反而增加了一项外部依赖,还会让用户产生“页面还不能用”的心理暗示。

外部资源改为选择性自托管

站点原先从 jsDelivr、cdnjs 等公共 CDN 加载主题依赖。在国内网络环境下,这种依赖很容易成为短板。

我没有把整个 NexT 插件包全部复制到站点。第一次尝试全量本地化后,生成目录明显变大,很多从未启用的库也被一起带了进来。后来改成只托管实际使用的资源:

  • Font Awesome
  • anime.js
  • Medium Zoom
  • Lozad
  • 本地搜索脚本
  • Gitalk
  • Creative Commons 图标

CSS 和 JavaScript 文件增加了内容哈希,例如:

1
2
/css/main-a5ed5ab8.css
/lib/medium-zoom/medium-zoom.min-42012313.js

服务器对静态资源使用了一年 immutable 缓存。如果文件名不变,发布新版本后,浏览器可能长期使用旧文件。内容哈希解决了这个问题:内容变化,URL 也会变化。

另一个容易忽略的问题是 CSS 内联。原配置把约 57 KB 的主题 CSS 重复塞进每个 HTML 页面,不仅让页面膨胀,也失去了浏览器共享缓存。调整后,首页从约 99 KB 降到 42 KB 左右,所有页面共用一份带哈希的样式文件。

卡片不是越宽松越好

桌面主内容区宽度本来没有问题,浪费主要来自纵向间距。

优化前,一张短文章卡片约 350 px 高;移动端约 305.5 px。标题区下方、阅读全文按钮上方和卡片内边距叠加后,首屏只能完整看到两篇短文章。

我把几个关键值收紧:

1
2
3
4
5
6
7
8
9
10
11
12
13
.posts-expand .post-header {
margin-bottom: 24px;
}

.posts-expand .post-button {
margin-top: 16px;
}

@media (min-width: 992px) {
.post-block {
padding: 28px;
}
}

调整后,桌面短卡片高度约 268 px,1440×1000 的首屏可以完整显示三篇文章。移动端卡片约 269.5 px,390×844 的首屏可以完整显示两篇,并露出第三篇。

移动端还隐藏了站点副标题,头部高度从约 111 px 缩到 75 px。标题、菜单和搜索按钮仍然完整,没有横向溢出。

图片里藏着比代码更大的浪费

线上 logo.svg 一度接近 1.95 MB。它虽然叫 SVG,内部实际嵌入了一张 1542×1542 的 PNG,并不是真正的矢量图。

源码仓库里已经有一份真正的矢量版本,只有约 68 KB,部署时直接使用即可。

头像问题更明显:源码头像有 5.5 MB,而页面显示区域很小。用线上已经压缩并验证过的 256×256 版本替换后,文件降到约 100 KB,视觉上没有变化。

这次没有盲目追求“所有图片都转 WebP”。图标、头像、文章截图的用途不同,处理方式也应该不同。先找体积最大的异常资源,收益通常更高。

源码构建还踩了两个坑

第一个坑是文章更新时间。

Hexo 原配置使用文件系统 mtime 作为更新时间。Git 不保存文件修改时间,所以在服务器重新克隆仓库后,所有文章都会显示成“今天更新”。最终将配置改为:

1
updated_option: date

没有显式 updated 字段的文章回退到发布时间,不再因为重新拉取仓库而产生错误更新时间。

第二个坑是 PWA Manifest。图标放在 /images/,但 Manifest 使用了根目录路径,部署后会请求不存在的文件。修正为相对于 manifest.json 的路径后,浏览器才能正确找到图标:

1
2
3
{
"src": "./android-chrome-192x192.png"
}

AI Agent 在这次优化里做了什么

这次没有让 AI 直接“看一眼页面然后改 CSS”。实际流程更接近一次小型工程任务:

  1. 用浏览器自动化记录冷启动时间和元素可见时间;
  2. 读取线上生成文件,定位 Motion 和外部依赖;
  3. 找到真正的 Hexo 源码仓库,而不是直接修改 /var/www/hexo
  4. 先建立会失败的验收,再修改配置;
  5. 在桌面端和移动端测量卡片尺寸、视口宽度和溢出情况;
  6. 提交源码到 GitHub,再由腾讯云部署仓库更新线上静态文件;
  7. 发布前创建完整备份,发布后重新跑冷启动测试。

AI 适合做这种需要大量读取、测量和交叉检查的工作,但“能生成代码”不等于“可以直接上线”。第一次独立审查就发现,验收脚本在 public/ 不存在时会跳过生成结果检查,造成假通过。修复后,测试会先执行干净构建,并在生成目录缺失时明确失败。

后续复审还指出,验收脚本的路径边界和 HTML 解析可以更严格。这些问题没有影响当前线上页面,但说明测试代码也需要接受测试,不能因为它位于 test/ 目录就默认可信。

优化结果

最终线上冷启动复测结果如下:

指标优化前优化后改善
第一篇文章可见5124 ms476 ms约 90.7%
FCP4392 ms804 ms约 81.7%
DOMContentLoaded4172 ms810 ms约 80.6%
完整加载未单独记录2116 ms不再阻塞正文显示

第一篇文章可见时间从 5 秒以上降到 0.5 秒以内,约快 10.8 倍。比数字更直观的变化是:现在打开页面时,正文直接出现,不需要盯着空白区域等动画。

线上复测还确认了这些结果:

  • 桌面首屏完整显示三篇文章;
  • 390 px 宽度下没有横向滚动;
  • 站点 canonical 正确指向 https://snails.cafe/
  • 已启用的主题资源全部由本站提供;
  • Nginx 和所有哈希资源返回正常。

两份部署只保留一个源码源头

我的博客同时部署在 GitHub Pages 和腾讯云服务器。以前两边容易形成“看起来一样,实际配置不同”的状态。

这次调整后,GitHub 的 main 分支作为唯一源码源头:

1
2
3
本地电脑 ──push──> GitHub main
├──> GitHub Pages
└──> 腾讯云构建并部署

本地电脑只需要正常拉取源码:

1
2
3
git pull --rebase origin main
npm ci
npm run test:optimization

腾讯云也使用仓库级 Deploy Key 推送和拉取,不需要在服务器保存个人账号密码。

写在最后

这次优化没有换主题,也没有重做设计。真正有效的改动很朴素:别隐藏正文,减少不稳定的外部依赖,压缩异常资源,让缓存策略和文件版本保持一致。

AI Agent 节省了排查和验证时间,但最终结果仍然来自可复现的测量、自动化测试和上线后的真实复测。对个人博客来说,这套流程有点“重”,但它至少保证了一件事:下次改主题配置时,不需要再靠刷新页面和肉眼猜测有没有变快。